# доменные объекты: почему сущности пухнут Все знают что объекты должны быть маленькими, соблюдать принцип единой ответственности и все в таком роде. Так же мы не должны так уж часто менять их, предпочитая изменениям расширение (через композицию и отдельные точки расширения). Эти критерии разработчики часто отждествляют с поддерживаемы кодом, и даже смеют заявлять что "это по СОЛИДу". Есть еще один критерий - логика по работе с данными должна быть там, где эти данные собственно живут. Это главное определение инкапсуляции. С этим критерием начинаются сложности, так как в нашей системе данные обычно сгруппированы в сущности. Когда разработчики начинают класть логику в сущности довольно часто они попадают в ситуацию что соблюдение инкапсуляции ломает остальные правила, объекты начинают пухнуть от логики. Мы явно нарушаем принцип единой ответственности и в целом "не менять код а расширять его" становится почти невозможно. Как же так выходит и можно ли подружить все наши "best practices" вместе? ## Именование и выделение ролей Один из факторов, которые сильно влияют на принятие решений это нейминг. Люди почему-то сильно недооценивают этот фактор. В СНГ это можно было бы объяснить нехваткой словарного запаса и недостаточным уровнем английского языка. Однако такая проблема есть и у нэйтив спикеров, так что скорее всего проблема кроется в чем-то другом. Люди по природе своей любят обобщать. Мы любим использовать общие термины при обсуждении требований - "юзер перешел в карзину", "юзер оформил заказ", "юзер пригласил друга". Подобное обобщение приводит к весьма интересным эффектам как в коде так и с точки зрения приоритизации требований. ![](https://i.imgur.com/u1WUDxt.png) Если согласно этому разделению мы сделаем одну единственную сущность User которая обслуживает все эти юзкейсы, мы можем получить классический god object. Ситуацию в которой один и тот же объект используется по всему проекту и обслуживает огромное количество юзкейсов. Однако не все "пользователи" выполняют одну и ту же роль. Если мы начнем разбираться начнут всплывать детали поведения - одни пользователи пассивны и после регистрации уходят с сайта. Есть пользователи которые регулярно пользуются определенными услугами и т.д. Пользователи, которые приглашают своих друзей и учавствуют в реферальной программе явно имеют свою роль - Referrer. Пользователи, которые получают услуги - Customers. Пользователи которые пользуются услугами постоянно - Regular Customers. ![](https://i.imgur.com/8xW5qLx.png) Более детальное разделение ролей больше необходимо для приоритизации юзкейсов. Однако и для разработчиков это дает определенные подсказки для декомпозиции стэйта системы. ## Варианты разделения сущностей Одна из проблем произрастает из классической UML конструкции описывающей агрегацию (has a). Если у покупателя есть история заказов, значит у класса Customer должна быть коллекция оных. Или же если у пользователя есть email и пароль то логично предположить что они входят в один объект. Рассмотрим последний пример чуть подробнее. У нас есть пользователь, который логинится по email + password. При регистрации он должен подтвердить email. Если мы 3 раза вводим пароль неправильно, то мы должны выслать подтверждение на email и заблокировать логин. При смене email-а мы так же должны дождаться подтверждения нового адреса. ```php class User { const MAX_ATTEMPTS = 3; private string $id; private string $email; private ?string $emailConfirmationToken; private ?string $newEmail; private string $password; private int $failedLoginAttempts = 0; private ?string $accountOwnershipToken; private ?\DateTimeImmutable $lastLoginAttempt; private ?\DateTimeImmutable $lastLogin; public function login(string $password): boolean { $this->lastLoginAttempt = new \DateTimeImmutable(); if ($this->failedLoginAttempts > self::MAX_ATTEMPTS) { } if (!password_verify($password, $this->password)) { $this->failedLoginAttempts++; return false; } $this->failedLoginAttempts = 0; return true; } public function resetLoginAttempts() { $this->failedLoginAttempts = 0; } public function changePassword() { } } class ChangePasswordUseCase { public function __construct( private UserRepository $users ) {} public function __invoke(ChangePassword $request) { $user = $this->users->get($request->userId); if (!password_verify($request->currentPassword, $user->getPassword(), PASSWORD_DEFAULT)) { $user->incrementFailedLoginAttempts(); if ($user->) } $user->setPassword(password_hash($request->newPassword, PASSWORD_DEFAULT)); } } class ``` Многие разработчики отождествляют доменный объект с записью в табличке, которую менеджит их ORM. Буду откровенен - для меня это так же была проблема которая не давала мне разобраться с этой проблемой на протяжелнии как минимум 3-х лет. ## Принадлежность данных и поведения ![](https://i.pinimg.com/originals/2e/19/97/2e199786747c98e3f75915cf00da9f5b.jpg) У заказа есть покупатель. Классическая конструкция из UML описывающая аргегацию (has a), вроде бы все хорошо. Но эти штуки должны еще как-то мэпиться на нашу базу, и мы хотим целостность и все такое. Стандартная практика сделать FK между табличкой с покупателями и заказами. Так же как правило ORM позволяют реализовывать такие связи путем референса на сущность. В итоге в объекте Order появляется объект Customer.