Разделение агрегата

Обсуждаем, как правильно строить приложения
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

Re: Разделение агрегата

Сообщение Bio man » 2017.12.27, 05:40

sda писал(а):
2017.12.27, 02:12
Верно ли у меня сложилось впечатление, что вы рассматриваете детали подобные сравнению хешей паролей как бизнес-логику и хотите подобные детали инкапсулировать в доменном сервисе?
Вовсе нет. В домене у меня есть интерфейс

Код: Выделить всё

interface AuthenticationServiceInterface
{
    /**
     * @throws AuthenticationFailedException
     * @throws ExpiredException
     */
    public function authenticate(IdentityId $identityId): Identity;

    /**
     * @throws InvalidPasswordException
     */
    public function login(Email $email, Password $password, ExpirationTime $expirationTime): Identity;

    public function logout(IdentityId $identityId): void;
}
А в инфраструктуре его реализация.
А использование этого сервиса уже в сервисе приложения.

noLogicOnlyWar
Сообщения: 75
Зарегистрирован: 2017.07.04, 20:53

Re: Разделение агрегата

Сообщение noLogicOnlyWar » 2017.12.27, 12:20

В Implementing domain-driven design можно найти примеры отправки email по доменному событию в application сервисе.
Т.е. отправка email не безнес логика? Если доменный эксперт сказал - после n должно быть отправлено сообщение пользователю, то это разве не должно находить отражение в домене?
Владелец коммерческой компании врядли будет рассказывать о том, как в его бизнесе устроена генерация паролей. Это сугубо технические детали в которых бизнес-аналитик не разбирается и поскольку разработчик моделирует доменную модель реального бизнеса она должна быть лишена подобных технических деталей.
Про реализацию - это понятно. В домене порт(интерфейс) в инфраструктуре адаптер с конкретной реализацией. Опять же если у нас есть задача сгенерировать пароль, то мы вроде как должны это обозначить в домене. Точно так же у репозитория в домене есть метод nextId который реализуется в инфраструктуре, но тем не менее в домене он описан.
Прокинуть зависимость можно в application сервис, там же сгенерировать пароль и уже его передавать в доменную модель.
То есть app сервис будет зависеть от инфраструктурного слоя? или вы имеете ввиду, что в app слое есть порт к инфраструктуре?
Я так понимаю, application слой должен декларировать юз кейсы приложения и содержать координирующую логику, а у вас он довольно много делает.
Верно ли у меня сложилось впечатление, что вы рассматриваете детали подобные сравнению хешей паролей как бизнес-логику и хотите подобные детали инкапсулировать в доменном сервисе?
Нет, действие - логин юзера. Для этого предлагается доменный сервис.

sda
Сообщения: 331
Зарегистрирован: 2013.12.19, 09:29

Re: Разделение агрегата

Сообщение sda » 2017.12.27, 13:39

Bio man, я не прав. Вы правы. Надо делать доменный сервис. Вот кстати как раз пример сервиса аутентификации.
Я так понимаю, application слой должен декларировать юз кейсы приложения и содержать координирующую логику, а у вас он довольно много делает.
Снимаю шляпу. Перечитал сейчас главу о доменных сервисах. Действительно вы правы. Будет повод заняться рефакторингом.

anton_z
Сообщения: 353
Зарегистрирован: 2017.01.15, 15:01

Re: Разделение агрегата

Сообщение anton_z » 2017.12.27, 15:57

sda писал(а):
2017.12.27, 02:12
Эванс пишет, что программный код хорошо спроектированной доменной модели может прочитать бизнес-аналитик поскольку код модели превращается в тот самый ubiquitous language как раз из-за отсутствия технических деталей.
Это вызывает сильные сомнения, не встречал никогда таких людей.

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


Для определения, какому классу назначить обязанность по логину я бы воспользоваться шаблоном GRASP Information Expert. У кого больше всего данных для выполнения этой операции? У юзера - значит пусть будет User::login().

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

noLogicOnlyWar
Сообщения: 75
Зарегистрирован: 2017.07.04, 20:53

Re: Разделение агрегата

Сообщение noLogicOnlyWar » 2017.12.27, 17:32

anton_z писал(а):
2017.12.27, 15:57

Для определения, какому классу назначить обязанность по логину я бы воспользоваться шаблоном GRASP Information Expert. У кого больше всего данных для выполнения этой операции? У юзера - значит пусть будет User::login().
Несогласен, давайте разберем простейший логин, на вход поступает имя пользователя и пароль, задача:
а) проверить что юзер с таким логином существует
б) проверить что пароль совпадает с паролем найденного юзера.
Так вот откуда у юзера знания обо всех юзерах в системе? Почему юзер (а) имеет доступ к паролю юзера (б)?

Если бы существовал аггрегат System и имел коллекцию Users, тогда логично что System::login(). А юзер - имхо нелогично.
И еще User::login нарушает spr, тк если меняется способ логина то должен меняться и User.

Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

Re: Разделение агрегата

Сообщение Bio man » 2017.12.27, 20:32

А вот создание юзера. Нужно проверить, не занят ли email.
В апп сервисе проверять нельзя, ибо БЛ.
В самом юзере тоже, ибо доступ к репозиторию (хотя тут спорно, репозиторий это домен, и к нему можно обращаться из юзера).
Что, еще 1 доменный сервис создавать?
Или проверять в репозитории?

Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

Re: Разделение агрегата

Сообщение Bio man » 2017.12.27, 20:46

При создании нужно еще и пароль захешировать....

anton_z
Сообщения: 353
Зарегистрирован: 2017.01.15, 15:01

Re: Разделение агрегата

Сообщение anton_z » 2017.12.28, 02:41

Вы меня не поняли. Безусловно, вытаскивание юзера из базы надо делать не в самом юзере. Так как объект юзера еще не создан, незьзя вызвать его метод login. В контроллере, классе формы или прикладном сервисе вытаскиваете юзера по логину. Далее вызываете $user->login($password). Внутри хеширование, проверка, при ошибке - исключение, если все хорошо, запись времени логина и прочей нужной информации в базу, генерация токена.

SRP - это казуистика какая-то. Попробуйте назвать эту "одну обязанность класса" User или "одну причину для его изменения".

Создание Юзера:

Для UI:

Проверку можно сделать в классе формы, обратившись к нужному репозиторию или DAO в валидаторе.

Для бизнес-слоя:

$user = $users_dao->add($login, $email, ...);

Там и пароль захешируем. Если email уже занят - исключение.

sda
Сообщения: 331
Зарегистрирован: 2013.12.19, 09:29

Re: Разделение агрегата

Сообщение sda » 2017.12.28, 05:39

Bio man писал(а):
2017.12.27, 20:32
А вот создание юзера. Нужно проверить, не занят ли email.
В апп сервисе проверять нельзя, ибо БЛ.
В самом юзере тоже, ибо доступ к репозиторию (хотя тут спорно, репозиторий это домен, и к нему можно обращаться из юзера).
Что, еще 1 доменный сервис создавать?
Или проверять в репозитории?
Да, похоже надо доменный сервис делать. В юзере я бы не обращался к репозиторию. Вернон пишет, что не стоит в агрегаты внедрять зависимости. Когда так хочется сделать, скорее всего нужен доменный сервис. И похоже публикацию доменных событий надо делать через синглтон, так как Вернон пишет, что доменные сервисы не должны иметь состояния.

anton_z
Сообщения: 353
Зарегистрирован: 2017.01.15, 15:01

Re: Разделение агрегата

Сообщение anton_z » 2017.12.28, 08:42

sda писал(а):
2017.12.28, 05:39
Вернон пишет, что не стоит в агрегаты внедрять зависимости.
Я вот тоже это видел и не только у Вернона. Но нигде я не нашел внятного объяснения, почему этого нельзя делать. Пишут "так надо" или "это вытекает само собой из разделения на сущности и сервисы" или "это не по назначению", не объясняя почему такое назначение возникло и почему его необходимо соблюдать.

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

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

noLogicOnlyWar
Сообщения: 75
Зарегистрирован: 2017.07.04, 20:53

Re: Разделение агрегата

Сообщение noLogicOnlyWar » 2017.12.28, 12:30

anton_z писал(а):
2017.12.28, 02:41
SRP - это казуистика какая-то. Попробуйте назвать эту "одну обязанность класса" User или "одну причину для его изменения".
Возможно, но тем не менее попробую :)
Обязанность - инкапсуляция поведения пользователя. Причина для изменения - соответственно изменение поведения.
Если брать логин, то тут у нас пользователь не является субъектом, действия производит не он а над ним, и поэтому я считаю что логин ни как не является задачей юзера.


По повод валидации email'a - делаю так:
VO UserEmail::__construct($value, $userRepo);
Потому как мы же хотим чтобы наши сущности были всегда валидны, а если не объявить зависимость в констуркторе VO или сущности то как этого добиться? К тому же не видел доводов против такого подхода, кроме (написанного выше), но стоит заметить что тк мы не сохраняем ссылку на зависимость то ее время жизни отличается от времени жизни VO или сущности, вот такой компромисс. Ну а внедрение через метод у сущностей встречается сплошь и рядом, и у Вернона тоже.

sda
Сообщения: 331
Зарегистрирован: 2013.12.19, 09:29

Re: Разделение агрегата

Сообщение sda » 2017.12.28, 12:49

noLogicOnlyWar писал(а):
2017.12.28, 12:30
Ну а внедрение через метод у сущностей встречается сплошь и рядом, и у Вернона тоже.
Дайте пример, где у Вернона это можно найти.


anton_z
Сообщения: 353
Зарегистрирован: 2017.01.15, 15:01

Re: Разделение агрегата

Сообщение anton_z » 2017.12.28, 13:53

noLogicOnlyWar писал(а):
2017.12.28, 12:30
Возможно, но тем не менее попробую :)
Обязанность - инкапсуляция поведения пользователя. Причина для изменения - соответственно изменение поведения.
Если брать логин, то тут у нас пользователь не является субъектом, действия производит не он а над ним, и поэтому я считаю что логин ни как не является задачей юзера.
Извините за назойливость, я задам еще несколько дополнительных вопросов.
Вы при назначении обязанностей опираетесь целиком на реальный мир? У вас класс User представляет пользователя, сидящего за компьютером?
Каким поведением класс User может обладать, если даже логин вы у него отобрали?
Какие атрибуты есть у вашего класса User?

Я бы опирался на правила GRASP, а не на пространные рассуждения, может ли юзер логиниться сам или его должны логинить.

noLogicOnlyWar
Сообщения: 75
Зарегистрирован: 2017.07.04, 20:53

Re: Разделение агрегата

Сообщение noLogicOnlyWar » 2017.12.28, 14:05

anton_z писал(а):
2017.12.28, 13:53
Вы при назначении обязанностей опираетесь целиком на реальный мир? У вас класс User представляет пользователя, сидящего за компьютером?
Нет, класс юзер моделирует пользователя в системе, то есть опираюсь на описание системы.
anton_z писал(а):
2017.12.28, 13:53
Каким поведением класс User может обладать, если даже логин вы у него отобрали?
Зависит от контекста. Примеры можно посмотреть в том же ddd in php.
anton_z писал(а):
2017.12.28, 13:53
Я бы опирался на правила GRASP, а не на пространные рассуждения, может ли юзер логиниться сам или его должны логинить.
Ну тем мы с вами по разному трактуем spr принцип приминительно к конкретному классу, так же и с информационным экспертом.

sda
Сообщения: 331
Зарегистрирован: 2013.12.19, 09:29

Re: Разделение агрегата

Сообщение sda » 2017.12.28, 14:41

noLogicOnlyWar, вот что он пишет
Dependency injection of a Repository or Domain Service into an Aggregate should generally be viewed as harmful. The motivation may be to look up a dependent object instance from inside the Aggregate.
Я понимаю, что это не ваш случай, но всё же почему если бизнес-операция требует проверять уникальность емайла не создать для неё доменный сервис ? Сущность же всегда будет оставаться валидной так как операции меняющие емайл будут вынесены в доменный сервис.
Вокруг анемичных объектов-сущностей с рудиментарным поведением появляется пачка сервисов (фактически, процедур), делающих всю реальную работу.
Я согласен и Вернон об этом предупреждает. Но если выбирать между внедрением в сущность и созданием доменного сервиса я бы выбрал последнее. Благо это не так часто требуется. Обычно бизнес-логика нормально кладется в сущность без зависимостей. По моему мнению внедрение зависимостей делают апи сущностей менее выразительными.

noLogicOnlyWar
Сообщения: 75
Зарегистрирован: 2017.07.04, 20:53

Re: Разделение агрегата

Сообщение noLogicOnlyWar » 2017.12.28, 15:16

sda писал(а):
2017.12.28, 14:41
Я понимаю, что это не ваш случай, но всё же почему если бизнес-операция требует проверять уникальность емайла не создать для неё доменный сервис ?
Я не совсем уверен что речь идет о инъекции зависимостей. То есть если

Код: Выделить всё

class A{
 private $b;
 public function __construct(B $b){ $this->b = $b; }
}
То тут да, объект B является зависимость класса А. А если у нас просто метод A::doSomething(B $b) то это ведь не означает что весь класс (сущность) является зависимой от B, от B тут зависит только определенное действие. Ну это я так трактую, по крайне мере так удобно думать :)
Сущность же всегда будет оставаться валидной так как операции меняющие емайл будут вынесены в доменный сервис.
Для этого требуются определенные соглашения все таки, а если инкапсулировать логику в конструкторе - то нет. Для меня этот фактор решающий, пока что не придумал/узнал как лучше. Еще как альтернатива есть фабрики и фабричные методы.

anton_z
Сообщения: 353
Зарегистрирован: 2017.01.15, 15:01

Re: Разделение агрегата

Сообщение anton_z » 2017.12.28, 15:37

sda писал(а):
2017.12.28, 14:41
Я согласен и Вернон об этом предупреждает. Но если выбирать между внедрением в сущность и созданием доменного сервиса я бы выбрал последнее. Благо это не так часто требуется. Обычно бизнес-логика нормально кладется в сущность без зависимостей. По моему мнению внедрение зависимостей делают апи сущностей менее выразительными.
А вот я выбираю внедрение. Мне так гораздо удобнее и коллегам из команды тоже.
Последний раз редактировалось anton_z 2017.12.28, 15:41, всего редактировалось 1 раз.

anton_z
Сообщения: 353
Зарегистрирован: 2017.01.15, 15:01

Re: Разделение агрегата

Сообщение anton_z » 2017.12.28, 15:41

sda писал(а):
2017.12.28, 14:41

Я понимаю, что это не ваш случай, но всё же почему если бизнес-операция требует проверять уникальность емайла не создать для неё доменный сервис ?
Давайте будем учитывать высокую связность (high cohesion). Когда мы логически связанное поведение разбрасываем по нескольким разным классам, у нас не будет высокой связности. Основной признак - операции на данных одного и того же класса производятся в нескольких не связанных между собой классах. Это один из аргументов не в пользу создания доменного сервиса и создания нового класса на "каждый чих", как делаеют иногдд лишь бы не нарушать SRP.

noLogicOnlyWar
Сообщения: 75
Зарегистрирован: 2017.07.04, 20:53

Re: Разделение агрегата

Сообщение noLogicOnlyWar » 2017.12.28, 17:49

Мы хотим получить высокую связность между чем и чем? Если все инкапсулировать в 1 класс то и взаимодействовать он ни с кем не будет.
производятся в нескольких не связанных между собой классах
Почему AuthManager (вобщем доменный сервис для логина и тп) не связан с User?
Последний раз редактировалось noLogicOnlyWar 2017.12.28, 17:50, всего редактировалось 1 раз.

Ответить