Подписывайтесь на наш Telegram канал и будьте в курсе всех событий.
Объектно-ориентированное программирование

Геттеры и сеттеры в объектах

Разрушают инкапсуляцию, увеличивают связанность кода и приводят к процедурному стилю программирования.

0
Основы

В программировании существует разные подходы к организации кода.

В программировании существует множество разных подходов к организации кода. Например знакомый со школьной скамьи или университета процедурный стиль, когда программа описывается серией последовательных инструкций: «сделай 1, сделай 2, сделай 3».

step1($data);
step2($data);
step3($data);

Этот стиль программирования прост в освоении и долгие годы был основным в программировании в C, Pascal и других языках.

Однако в современном PHP и Laravel обычно применяется объектно-ориентированный подход. В котором мы создаём классы, инициируем их и используем методы, чтобы управлять поведением объектов. ООП основывается на идее, что данные и поведение должны быть объединены в единый «живой» объект. Например, представьте класс App:

$app = new App();

$app->start();

В этом случае мы не указываем пошагово, что необходимо делать, а поручаем объекту выполнить всю необходимую инициализацию и запуск (инкапсуляция). Такой подход позволяет скрыть детали реализации и предоставляет лишь интерфейс для взаимодействия с объектом.

Но существует чертовски много кода и практик на ООП языках, который является процедурным по своей конструкции. Поэтому мы разберем несколько примеров.

1
Глупые объекты рождают процедурные участки кода

Объект как контейнер для данных.

Встречается практика, когда один объект используется исключительно как контейнер для данных – он не содержит бизнес-логики, а только хранит информацию. Рассмотрим пример:

class User
{
    private string $name;
    private string $email;

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setEmail(string $email): void
    {
        $this->email = $email;
    }
}

Часто такие классы называют «Data Transfer Object» (DTO), однако стоит отметить, что в классическом понимании DTO рекомендуется делать иммутабельными — без сеттеров, чтобы обеспечить неизменность передаваемых данных.

Здесь объект User используется только для хранения данных. Дальше эти данные обрабатываются в различных частях приложения. Например:

// В одном участке кода создаем объект и заполняем его данными
$user = new User();
$user->setName('John Doe');
$user->setEmail('john.doe@example.com');

// В другом участке кода валидируем данные
$validator = new Validator();
$validator->validate($user);

// В третьем участке кода отправляем письмо пользователю
$notification = new WelcomeNotification();
$notification->send($user->getEmail());

В этом примере мы видим, что объект User не обладает собственной логикой – за обработку данных отвечают внешние классы, такие как Validator и Mailer. По сути, вместо использования массива для хранения информации мы применяем объект, но бизнес-логика вынесена за его пределы. Это приближает архитектуру к процедурному стилю, где программа состоит из набора функций, выполняющих действия над данными.

Аналогичная ситуация наблюдается и в ORM, которые не используют подход Active Record, например, в Doctrine:

$entityManager = EntityManager::create($connection, $config);

// Создаем объект пользователя
$user = new User();
$user->setName("John Doe");
$user->setEmail("john.doe@example.com");

// Сохраняем в БД
$entityManager->persist($user);
$entityManager->flush();

Это очень сильный архитектурный компромисс, который отсупает от принцыпов объектно-ориентированного программирования, так как объекты должны быть ответственными за свои данные и поведение.

В результате мы получаем систему, где логика распределена между «глупыми» объектами которые хранят только данные, и отдельными сервисами и/или процедурными участками кода, работающими с этими данными. Это затрудняет понимание, тестирование и сопровождение кода, а также увеличивает связанность компонентов.

2
Tell, Don’t Ask

Объекты должны выполнять действия, а не только отдавать свои данные

Tell, Don’t Ask – это один из хорошо сформулированных принципов качественного объектно-ориентированного проектирования, который помогает создать чистую и легко поддерживаемую архитектуру.

«Не спрашивай объект о данных, чтобы принять решение – скажи объекту, что делать.»

На первый взгляд этот принцип может показаться противоречащим принципу единой ответственности (SRP), но в реальной разработке зачастую приходится делать выбор, какой из принципов важнее в конкретном контексте. Тем не менее, чем больше принципов удается соблюсти одновременно, тем лучше.

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

$user->notify(WelcomeNotification::class);

Здесь объект самостоятельный субъект, который выполняет действия сам, а не просто предоставляет данные для внешней обработки.

К тому же принцип Tell, Don’t Ask хорошо сочетается с Законом Деметры (Law of Demeter, LoD), который также способствует созданию более устойчивых и независимых объектов.

Можно узнать больше из работ Мартина Фаулера, Аллена Холуба и Егора Бугаенко.