Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions public/img/ui/bone.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions resources/views/library/getters-and-setters.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@extends('layout')
@section('title', 'Геттеры, сеттеры и DTO')
@section('description', 'Разрушают инкапсуляцию, увеличивают связанность кода и приводят к процедурному стилю программирования.')
@section('content')

<x-header align="align-items-center">
<x-slot name="sup">Объектно-ориентированное программирование</x-slot>
<x-slot name="title">Геттеры, сеттеры и DTO</x-slot>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Очень хороший голосовой комментарий от Чубарова, в котором он указал, что я поддался распространённому заблуждению. Мой пример с классом User не является DTO, поскольку как DTO, так и VO должны быть имутабельными, так как у него есть сеттер, то он уже не может считаться таковым.

Нужно добавить будет сноску об этом, а заголовок поменять

<x-slot name="description">
Разрушают инкапсуляцию, увеличивают связанность кода и приводят к процедурному стилю программирования.
</x-slot>
<x-slot name="content">
<img src="/img/ui/bone.svg" class="img-fluid d-block mx-auto">
</x-slot>
</x-header>

@php
$sections = collect([
'basics',
'getters-and-setters',
'tell-dont-ask',
])
->map(fn ($file) => \Illuminate\Support\Str::of($file)->start('getters-and-setters/'))
->map(fn ($file) => new \App\Library($file));
@endphp

@include('particles.library-section', ['sections' => $sections])
@endsection
28 changes: 28 additions & 0 deletions storage/library/getters-and-setters/basics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
title: "Основы"
description: "В программировании существует разные подходы к организации кода."
---

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

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

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


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

```php
$app = new App();

$app->start();
```

В этом случае мы не задаем пошагово, что делать, а поручаем объекту выполнить всю необходимую инициализацию и запуск.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Возможно, стоит сразу написать, что это называется инкапсуляция, чтобы читатель запоминал терминологию

Такой подход позволяет скрыть детали реализации и предоставляет лишь интерфейс для взаимодействия с объектом.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Я бы перефразиваровал

82 changes: 82 additions & 0 deletions storage/library/getters-and-setters/getters-and-setters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
title: "Глупые объекты рождают процедурные участки кода"
description: "Объект как контейнер для данных."
---

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

```php
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;
}
}
```

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

```php
// В одном участке кода создаем объект и заполняем его данными
$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`:

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

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

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

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

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


93 changes: 93 additions & 0 deletions storage/library/getters-and-setters/tell-dont-ask.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
title: "Tell, Don’t Ask"
description: "Объекты должны выполнять действия, а не только отдавать свои данные"
---

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

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

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

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

Вместо того чтобы извлекать данные и проверять их во внешнем коде, объект сам выполняет необходимое действие.

```php
class User
{
protected string $name;
protected string $email;

public function register(): void
{
$validator = Validator::make($data, [
'name' => 'required',
'email' => 'required|email',
]);

if ($validator->fails()) {
throw new RuntimeException("Данные не корректны");
}

//...

$this->notify(WelcomeNotification::class)
}
}


$user = new User(
name: "John Doe",
email: "john.doe@example.com"
);

$user->register();
```

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


Если объект не должен нести на себе всю логику, её можно вынести в сервис, передавая туда объект:

```php
class UserService
{
public function __construct(
private Validator $validator,
private Mailer $mailer
) {}

public function register(array $data): User
{
$this->validator->validate($data, [
'name' => 'required',
'email' => 'required|email',
]);

if (this->validator->fails()) {
throw new RuntimeException("Данные не корректны");
}

$user = new User($data);

//...

$this->mailer->sendWelcomeMessage($user->email);

return $user;
}
}

$user = app(UserService::class)->register([
'name' => 'John Doe',
'email' => 'john.doe@example.com',
]);
```

Здесь объект `UserService` выполняет логику, а объект `User` остаётся максимально простым.
Copy link
Member Author

@tabuna tabuna Mar 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это классический пример, который часто используют в качестве иллюстрации. Однако на практике я нередко встречал, что существует один общий UserManager или UserService, который не ограничивается одной конкретной задачей, а скорее большим декоратором над глупым объектом в попытке обратно вернуться к умному классу.

Например:

$manager = new UserManager();

// Получить всех пользователей
$manager->getAll();

// Отправить приветственное письмо 
$manager->sendWelcomeEmail($user);

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

// Класс относящийся к выборке пользователей
$usersManager = new UsersManager();
$usersManager->getAll();

// Класс относящийся уже к уведомлениям пользователей
$userNotification = new UserNotification();
$userNotification->sendWelcomeEmail($user);

Как более однозначный процедурный (!) предложить однозначный способ Action и добавить перекрёстную ссылку на него?



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

Можно узнать больше из работ [Мартина Фаулера](https://martinfowler.com/bliki/TellDontAsk.html), [Аллена Холуба](https://www.infoworld.com/article/2161183/why-getter-and-setter-methods-are-evil.html) и [Егора Бугаенко](https://www.yegor256.com/2014/09/16/getters-and-setters-are-evil.html).
Loading