Программа курса← Объектно ориентированное программирование
1История ООП
2Объект и Класс
3Базовые концепции ООП
4Принцип проектирования GRASP
5Принцип проектирования SOLID
6Паттерны GOF

Поля, свойства и методы

Класс описывает две вещи: что объект хранит и что он умеет. Первое – поля, второе – методы. Между ними стоят свойства: снаружи они выглядят как данные, а внутри работают как методы.

Поля – где живут данные

Вернёмся к чертежу машины. На чертеже написано, что у машины есть цвет и модель. Но сам чертёж не бывает красным: цвет появляется только у конкретной построенной машины.

Поле – это переменная, которая принадлежит объекту. Класс объявляет её один раз, а значение у каждого объекта своё:

  • у красной Tesla поле color хранит "красный", у синей Toyota – "синий";
  • изменение поля одного объекта не трогает остальные;
  • поле живёт ровно столько, сколько живёт сам объект.

Поля почти всегда закрывают от внешнего кода. Причина простая: если пробег машины может поменять кто угодно и на что угодно, объект перестаёт отвечать за собственную корректность.

Зачем закрывать поля и где проходит граница – в уроке Инкапсуляция.

public class Car
{
    // Поля объявлены в классе, но значения у каждого объекта свои
    private string _color;
    private int _mileage;
}
class Car {
private:
    // Поля объявлены в классе, но значения у каждого объекта свои
    std::string color_;
    int mileage_ = 0;
};
type Car struct {
	Color   string // с большой буквы – видно снаружи пакета
	mileage int    // с маленькой – только внутри
}
class Car:
    def __init__(self, color):
        self._color = color  # поле создаётся присваиванием в конструкторе
        self._mileage = 0    # подчёркивание – договорённость «снаружи не трогать»
class Car {
    readonly color: string;
    #mileage = 0; // решётка закрывает поле на уровне языка

    constructor(color: string) {
        this.color = color;
    }
}
public class Car {
    // Поля закрыты: снаружи к ним не обратиться
    private String color;
    private int mileage;
}
class Car(val color: String) {
    // Поле под свойством Kotlin создаёт сам, когда оно нужно
    private var mileage: Int = 0
}

Методы – что объект умеет

На чертеже написано не только из чего машина состоит, но и что она делает: заводится, едет, тормозит. Едет при этом не чертёж – едет конкретная машина, и пробег накручивает себе она же.

Метод – это функция, которая живёт внутри класса. Отличие от обычной функции одно, но принципиальное: метод имеет доступ к полям своего объекта и обычно вызывается «у него».

Когда мы пишем myCar.Drive(120), мы не просим программу «проехать 120 километров» – мы просим об этом конкретную машину. Она сама решит, что при этом изменится внутри.

Метод – хорошее место для проверок. Пробег нельзя увеличить на отрицательное число, и знает об этом сам объект, а не тот, кто его вызвал.

public void Drive(int km)
{
    if (km <= 0) return; // проверка живёт внутри объекта
    _mileage += km;      // метод видит поля своего экземпляра
}
void Drive(int km) {
    if (km <= 0) return; // проверка живёт внутри объекта
    mileage_ += km;      // метод видит поля своего экземпляра
}
// Получатель c – тот самый объект, «у которого» вызвали метод
func (c *Car) Drive(km int) {
	if km <= 0 {
		return
	}
	c.mileage += km
}
def drive(self, km):     # self – объект, у которого вызвали метод
    if km <= 0:
        return
    self._mileage += km
drive(km: number): void {
    if (km <= 0) return; // проверка живёт внутри объекта
    this.#mileage += km;
}
public void drive(int km) {
    if (km <= 0) return; // проверка живёт внутри объекта
    mileage += km;
}
fun drive(km: Int) {
    if (km <= 0) return // проверка живёт внутри объекта
    mileage += km
}

Свойства – поле снаружи, метод внутри

Как мы говорили выше, поля закрывают внутри объекта. Но данные всё равно кому-то нужны: покупателю – цвет, сервису – пробег. Классический выход – методы-обёртки: GetColor для цвета, GetMileage для пробега.

SetColor и SetMileage в этом списке нет, и это не забывчивость. Цвет машине задают на заводе, пробег накручивает только поездка – значит, наружу отдано одно чтение: нет метода – нет и способа изменить поле. Так объект решает не «показывать или прятать», а что именно позволено делать с каждым значением: что-то отдаётся на чтение, что-то меняется методом вроде Drive, а что-то не выходит наружу вовсе.

Обёрток при этом много: на каждое значение свой метод, а если менять всё-таки разрешено – то и второй. И обращение выглядит как вызов, хотя по смыслу это просто данные. Поэтому и придумали свойства.

Свойство выглядит в коде как поле, но за чтением и записью стоит метод. Значит, внутрь можно спрятать проверку, пересчёт или запрет на изменение – и внешний код об этом не узнает, потому что обращение к нему не меняется.

Механизм есть не везде: часть языков поддерживает свойства прямо в синтаксисе, а часть по-прежнему заставляет писать методы-обёртки руками. Как это устроено в выбранном языке:

public string Color { get; }    // только чтение: задаётся в конструкторе
public int Mileage => _mileage; // только чтение: меняет метод Drive
// Свойств в C++ нет, их роль играют методы
const std::string& color() const { return color_; }
int mileage() const { return mileage_; }
// Сеттеров нет ни у того, ни у другого: снаружи оба значения только читают
// Свойств в Go нет. Открытое поле читают напрямую, а «только для чтения» –
// это закрытое поле и метод к нему
func (c *Car) Mileage() int {
	return c.mileage
}
@property
def mileage(self):    # обращение как к полю: car.mileage
    return self._mileage
get mileage(): number { // обращение как к полю: car.mileage
    return this.#mileage;
}
// Свойств в языке нет: по соглашению пишут геттеры и сеттеры
public String getColor() { return color; }
public int getMileage() { return mileage; }
// Сеттеров нет ни у того, ни у другого: снаружи оба значения только читают
val color: String = "белый" // val – читать можно, менять нельзя
var mileage: Int = 0
    private set             // менять может только сам класс

В C# свойства встроены в язык – это { get; set; } в объявлении.В C++ свойств нет: наружу значения отдают обычные методы.В Go свойств нет вовсе: есть поля и методы, а видимостью управляет регистр первой буквы имени.В Python это декоратор @property над обычным методом.В TypeScript это аксессоры get и set.В Java свойств нет: их роль по соглашению играют геттеры и сеттеры.В Kotlin свойство – основной способ объявить данные, а поле появляется само, если оно нужно.

Поле, свойство, метод: в чём разница

Поле Свойство Разница смысловая, а не техническая: свойство – это метод, и внутрь можно положить хоть запрос в базу. Технически ничто не мешает, но car.mileage выглядит как чтение данных, и долгой работы за ним никто не ждёт – примерно как выключатель на стене, который вместо света вызывает лифт. Метод
Что это переменная объекта доступ к данным через код действие объекта
Как выглядит вызов car.color car.color car.drive(120)
Можно ли добавить проверку нет да да
Что обычно означает состояние состояние поведение

Ещё раз, на всякий случай: таблица про смысл, а не про синтаксис – набор механизмов сильно зависит от языка. Свойств нет в C++, Go и Java – там их роль играют методы. А в Си нет и методов: есть структуры с данными и отдельные функции, которые эти структуры принимают аргументом.

Практическое правило: если обращение выглядит как получение данных, это свойство; если как поступок – это метод. Поэтому car.mileage – свойство, а car.drive(120) – метод, хотя оба в итоге работают с одним и тем же полем.

Пример: класс Car целиком

Здесь есть все три: закрытое поле с пробегом, свойство только для чтения и метод, который меняет пробег и следит за корректностью.

public class Car
{
    // Поле – переменная внутри объекта, снаружи она не видна
    private int _mileage;

    public Car(string color, string model)
    {
        Color = color;
        Model = model;
    }

    // Свойства: выглядят как поля, но за ними стоят методы
    public string Color { get; }
    public string Model { get; }

    // Только для чтения: изменить пробег извне нельзя
    public int Mileage => _mileage;

    // Метод – действие, которое объект умеет выполнять
    public void Drive(int km)
    {
        if (km <= 0)
        {
            Console.WriteLine("Расстояние должно быть больше нуля");
            return;
        }

        _mileage += km;
        Console.WriteLine($"{Model} проехал {km} км, всего {_mileage} км");
    }
}
class Car {
public:
    Car(std::string color, std::string model)
        : color_(std::move(color)), model_(std::move(model)) {}

    // Свойств в C++ нет: доступ дают методы
    const std::string& color() const { return color_; }
    const std::string& model() const { return model_; }

    // Только для чтения: сеттера просто нет
    int mileage() const { return mileage_; }

    // Метод – действие, которое объект умеет выполнять
    void Drive(int km) {
        if (km <= 0) {
            std::cout << "Расстояние должно быть больше нуля" << std::endl;
            return;
        }

        mileage_ += km;
        std::cout << model_ << " проехал " << km
                  << " км, всего " << mileage_ << " км" << std::endl;
    }

private:
    // Поля – переменные внутри объекта, снаружи не видны
    std::string color_;
    std::string model_;
    int mileage_ = 0;
};
// Свойств в Go нет: есть поля и методы. Видимостью управляет регистр первой буквы –
// mileage с маленькой буквы не виден за пределами пакета.
type Car struct {
	Color   string
	Model   string
	mileage int
}

func NewCar(color, model string) *Car {
	return &Car{Color: color, Model: model}
}

// Роль «свойства только для чтения» играет обычный метод
func (c *Car) Mileage() int {
	return c.mileage
}

func (c *Car) Drive(km int) {
	if km <= 0 {
		fmt.Println("Расстояние должно быть больше нуля")
		return
	}

	c.mileage += km
	fmt.Printf("%s проехал %d км, всего %d км\n", c.Model, km, c.mileage)
}
class Car:
    def __init__(self, color, model):
        self._color = color
        self._model = model
        # Подчёркивание – договорённость: «снаружи не трогать»
        self._mileage = 0

    # Свойство: снаружи выглядит как поле, внутри это метод
    @property
    def color(self):
        return self._color

    @property
    def model(self):
        return self._model

    @property
    def mileage(self):
        return self._mileage

    # Метод – действие, которое объект умеет выполнять
    def drive(self, km):
        if km <= 0:
            print("Расстояние должно быть больше нуля")
            return

        self._mileage += km
        print(f"{self._model} проехал {km} км, всего {self._mileage} км")
class Car {
    // Поле с решёткой закрыто на уровне языка, а не по договорённости
    #mileage = 0;

    constructor(
        public readonly color: string,
        public readonly model: string,
    ) {}

    // Свойство: обращаются как к полю, а работает метод
    get mileage(): number {
        return this.#mileage;
    }

    drive(km: number): void {
        if (km <= 0) {
            console.log("Расстояние должно быть больше нуля");
            return;
        }

        this.#mileage += km;
        console.log(`${this.model} проехал ${km} км, всего ${this.#mileage} км`);
    }
}
public class Car {
    private final String color;
    private final String model;
    // Поле – данные объекта, наружу закрыто
    private int mileage;

    public Car(String color, String model) {
        this.color = color;
        this.model = model;
    }

    // Свойств в языке нет: доступ дают геттеры
    public String getColor() {
        return color;
    }

    public String getModel() {
        return model;
    }

    public int getMileage() {
        return mileage;
    }

    public void drive(int km) {
        if (km <= 0) {
            System.out.println("Расстояние должно быть больше нуля");
            return;
        }

        mileage += km;
        System.out.println(model + " проехал " + km + " км, всего " + mileage + " км");
    }
}
class Car(val color: String, val model: String) {
    // Свойство с закрытым сеттером: читать могут все, менять – только сам класс
    var mileage: Int = 0
        private set

    fun drive(km: Int) {
        if (km <= 0) {
            println("Расстояние должно быть больше нуля")
            return
        }

        mileage += km
        println("$model проехал $km км, всего $mileage км")
    }
}

Почему пробег закрыт

Попробуйте мысленно открыть поле mileage наружу. Ничего не сломается сразу – но теперь любой код в программе может выставить пробег в ноль или в минус тысячу. И когда это случится, искать виноватого придётся по всему проекту.

Пока пробег меняет только метод drive, у ошибки одно место. Это и есть смысл границы между «внутри объекта» и «снаружи»: не спрятать данные, а сузить круг подозреваемых.

Итог: поля – это память объекта, методы – его поведение, свойства – аккуратная дверь к памяти. Разница между свойством и методом не техническая, а смысловая: свойство отвечает на вопрос «какой объект сейчас», метод – «что он умеет сделать».