Как реализуете репозиторий (Doctrine, Analogue, ручками, etc)

Юрий Быков

Новичок
Для меня модель это данные определённой сущности и их обработка в пределах ответственности, события же я вижу как первопричину для изменения данных модели через обработку (бизнес-логику), т.е. события это отдельно от модели сущности. События снаружи модели.

Волшебный пендель -> User -> генерация события(команды) -> изменение данных -> персистентость.

Методы этого точно не относятся к объекту Route или Leg из TripPlanner, поэтому вижу в этом нарушение SRP:
https://github.com/qandidate-labs/broadway/blob/master/src/Broadway/EventSourcing/EventSourcedAggregateRoot.php
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
уже N раз написали, что события регистрируется в некотором Аггрегаторе, а не отправляется напрямую в модели по отдельности

модели-сущности или подписаны в аггрегаторе на уведомления о событиях, или создаются на лету моделью более высокого уровня абстракции, которая подписана на события
 
Последнее редактирование:

fixxxer

К.О.
Партнер клуба
А когда ты из приложения дергаешь модель, которая делает return $value, в этом самом return $value ты тоже видишь нарушение srp? :)
 

Юрий Быков

Новичок
уже N раз написали, что события регистрируется в некотором Аггрегаторе, а не отправляется напрямую в модели по отдельности
да, видел, но
Во-первых, именно модель знает о том, что же там внутри нее изменилось. Поэтому это логично записывать события внутри нее, а не снаружи. Это пока не требует именно ES-подхода, но очень близко к нему (мы просто записываем ->recordThat(new EventWasBuzzed() события по мере необходимости) .
Аггрегатор - это та же сущность, в которой собраны методы для обработки нижестоящих доменных моделей. Т.е. события внутри моделей, или аггрегатора, который тоже модель. Если события делать вне модели то аггрегатор перерождается в сервис, который дёргает бизнес-логику, в таком случае, если модель не вернёт дельту изменений мы не будем знать, что писать в БД.
А когда ты из приложения дергаешь модель, которая делает return $value, в этом самом return $value ты тоже видишь нарушение srp?
Не понял вопрос, модель сама ничего не возвращает, у неё наружу API для её изменения.
 

Юрий Быков

Новичок
Если я правильно понял подход, то в ES пишем в БД не информацию из объектов, а то что накопилось в коллекторе событий. Т.е. все события содержат diff, который пишется в БД. Идёт 2 потока: модель с данными, они нужны только для текущей операции, и события, которые потом обрабатываются, собирается SQL и в БД.
 

Вурдалак

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

Допустим, пользователь может находиться в состояниях «На модерации»/«Не на модерации».

Пользователь регается (UserWasRegistered) и тут же отправляется на модерацию (UserWasSentForModeration).
Пользователь меняет имя (UserWasRenamed) и тут же отправляется модерацию (UserWasSentForModeration), если только уже не на ней по какой-то причине.

Как ты себе представляешь события вне сущности?

PHP:
// RegisterUserHandler

$user = User::register($id, $name, ...);
$this->userRepository->save($user);

$this->eventDispatcher->dispatch(new UserWasRegistered($id, $name, ...));
$this->eventDispatcher->dispatch(new UserWasSentForModeration($id));

// RenameUserHandler

$beforeStatus = $user->getStatus();

$user->rename($newName);
$this->userRepository->save($user);

$this->eventDispatcher->dispatch(new UserWasRenamed($id, $newName));
if ($beforeStatus !== $user->getStatus() && $user->getStatus() === UserStatus::MODERATION) {
    $this->eventDisptacher->dispatch(new UserWasSentForModeration($id));
}
У тебя не закрадывается впечатление того, что:

1. Мы нарушаем инкапсуляцию User. Мы не можем знать, что же там внутри произошло, вот этот if ($user->getStatus() ...) — это явный признак, что логика протекла из User'а.
2. Всё это выглядит очень хрупко: достаточно немного измениться логике в register/rename и событие может срабатывать «ложно» (или, наоборот, не будет), про него могут просто «забыть»?

Собственно, второе — это следствие первого.

Ещё момент: иногда мы можем к событию прикреплять какие-то данные, которые «снаружи» не поступают, которые вычисляются в самой модели на основе входных данных. Чтобы эти данные получить, возможно опять потребуется выставить кишки сущности наружу. События же позволяют это сделать элегантно.
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
А, да, я ещё забыл про идемпотентность: если имя пользователя уже совпадает с тем, что пришло в $newName, то мы не должны ничего генерировать вообще. Это дополнительные if'ы вне сущности.
 

Вурдалак

Продвинутый новичок
Для меня модель это данные определённой сущности и их обработка в пределах ответственности
Я спрашивал тебя в чём «ответственность» (responsibility) модели, чтобы можно было формально понять что значит «нарушение SRP» в отношении модели.

А что такое ответственность User'а для тебя? Регистрироваться — это ответственность? Переименовываться — это ответственность? Уж не означает ли это, что сущность всегда должна иметь ровно один метод, ведь иначе автоматически будет то самое нарушение? Нет, конечно. Это не ответственности в том смысле, в котором можно говорить про SRP. У User'а ответственность — моделировать соответствующее понятие из какого-то определенного domain'а.

Вот если бы мы захотели там какие-нибудь счетчики для статистики понатыкать прямо в классе модели — тут будет явное нарушение, потому что эти счетчики к моделированию никак не будут относиться. События же — часть модели, и для меня немного странно говорить про нарушение SRP.
 

fixxxer

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

Вурдалак

Продвинутый новичок
Ну, в принципе, на счетчики может быть и бизнес-логика завязана. Правда, чтобы это было еще и в одном и том же контексте, пример придумать сложно
Ну я имел в виду а-ля Counter::getInstance()->count('user_register') или какой-то подобный ужас прямо в сущности.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Аггрегатор - это та же сущность
А джунгли - это тот же банан. Нет, конечно, агрегатор событий и сущность - это разные слои.

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

модель сама ничего не возвращает, у неё наружу API для её изменения
модель может что-то возвращать - DTO, VO, другие модели
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
Тут параллельно происходит ещё какая-то дискуссия, которую я не могу понять...

Aggregate root (не это ли тут называют «агрегатором»?) — это действительно модель и есть.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
@Вурдалак, не, не сегодня )))
да, модель, но другой уровень
дай лучше ссылок на хорошие статьи или монографии по теме, может, почитаю на выходных
 

Юрий Быков

Новичок
А джунгли - это тот же банан. Нет, конечно, агрегатор событий и сущность - это разные слои.
Ранее в теме упоминал про коллектор событий, имелся ввиду инстанс, с одним из аттрибутов - массив, в котором накапливаются события, в контексте вышеупомянутого проекта это массив $queue из
https://github.com/qandidate-labs/broadway/blob/master/src/Broadway/EventHandling/SimpleEventBus.php
Что вкладывается в понятие "агрегатор событий" я не понимаю тогда. Как/где располагается этот слой в архитектуре? Мы может в терминологии разошлись?
Если вспомнить, что кроме модели есть еще и контроллер, а сама модель может быть наблюдателем, то все становится на свои места.
Я сторонник подхода худого контроллера. Контроллер генерирует команду (это именно объект), которая уходит либо в сервис либо в CommandBus с последующим вызовом соответствующего CommandHandler. Зачем подписывать модель на события контроллера? Слой доменной логики, вообще, не должен знать, что там выше и не должен обрабатывать события верхнего уровня, иначе нарушается луковая архитектура.
модель может что-то возвращать - DTO, VO, другие модели
На данный момент, действительно, что мне пригодилось для возврата, так это getId(), потому что id генерировала БД (mysql), т.к. по REST, если отправляешь POST на /users/ с данными новой учетной записи, то назад должен получить 201 код и ссылку на эту новую запись /users/:id. Но если использовать VO UserId с внешним генератором ID, то и этот метод не нужен был бы. А согласно CQRS при команде вообще ничего возвращать не нужно.
$user = new User(UserId $userId, string $name, string email);
Doctrine 2 вытаскивает данные через рефлексию, ей данный метод не нужен.
Может - да может, нужно - вопрос, требуется конкретный пример. Для read-модели данные отдельно заполняются.
DTO возвращать точно не нужно, это задача хендлера/сервиса, который получает данные из репозитория, и только при Query-запросе.

Вурдалак, благодарю за развёрнутый комментарий и примеры кода.
Я спрашивал тебя в чём «ответственность» (responsibility) модели, чтобы можно было формально понять что значит «нарушение SRP» в отношении модели.
Мне понравился вариант из статьи (см. пункт "Заключение") "оперирует данными и объектами, которые находятся внутри домена"
http://blog.byndyu.ru/2010/05/domain-driven-design.html
События я вижу как внешнее по отношению к сущности явление, скорее всего из-за того что в PHP мы не можем как-то элегантно это обработать. Хотя они больше должны быть похожи на Exception, только в положительном ключе и, следовательно, находиться внутри. Мне не нравится именно тот факт что Entity знает про некий инстанс, который обрабатывает события.
Но как ты сказал выше это именно event-модель, т.е. ключевое слово event и без их обработки в этом случае не обойтись. Я не против данного решения.

Приходит в голову такое решение:

PHP:
class User {
...
public function activate() : UserWasActivatedEvent
{
$this->is_active = true;
$this->actived_at = new DateTime();

return new UserWasActivatedEvent($this->id, $this->is_active, $this->actived_at);
}
}
...
$event = $user->activate();
$this->eventDispatcher->dispatch($event);
Вот, аналогия с Exception мне больше нравится, его мы тоже снаружи ловим через try-catch.
 

Вурдалак

Продвинутый новичок
Мне понравился вариант из статьи (см. пункт "Заключение") "оперирует данными и объектами, которые находятся внутри домена"
http://blog.byndyu.ru/2010/05/domain-driven-design.html
Я бы это назвал «классической» ООП-моделью. Но когда в дело вступают события, то приходится немного изменить сознание.
Тебе ничего не мешает автора этого поста спросить на тему ES и SRP, он есть в Твиттере (правда, не знаю отвечает ли он).

События я вижу как внешнее по отношению к сущности явление, скорее всего из-за того что в PHP мы не можем как-то элегантно это обработать. Хотя они больше должны быть похожи на Exception, только в положительном ключе и, следовательно, находиться внутри. Мне не нравится именно тот факт что Entity знает про некий инстанс, который обрабатывает события.
Что ты вкладываешь в понятие «обрабатывать событие»? Мы же не вызываем никаких подписчиков на данном этапе, мы фактически лишь складываем их в массив. Мне кажется, ты этот момент упустил.

Приходит в голову такое решение:

PHP:
class User {
...
public function activate() : UserWasActivatedEvent
{
$this->is_active = true;
$this->actived_at = new DateTime();

return new UserWasActivatedEvent($this->id, $this->is_active, $this->actived_at);
}
}
...
$event = $user->activate();
$this->eventDispatcher->dispatch($event);
Тогда нужно возвращать массив событий, их может быть несколько.

Вот, аналогия с Exception мне больше нравится, его мы тоже снаружи ловим через try-catch.
Так в моём подходе я dispatch тоже снаружи вызываю.

Отличий от exceptions тут действительно минимум. Только исключение может быть ровно одно, а событий — несколько, поэтому-то мы и складываем события в массив, это я и называю «записывать событие». До подписчиков эти события уже дойдут после персистенции, т.е. грубо говоря
PHP:
$user->activate();

$this->userRepository->save($user);
$this->eventDispatcher->dispatch($user->getEvents());
 

Юрий Быков

Новичок
Что ты вкладываешь в понятие «обрабатывать событие»? Мы же не вызываем никаких подписчиков на данном этапе, мы фактически лишь складываем их в массив. Мне кажется, ты этот момент упустил.
public static function register(TripIdentity $identity, string $name): Trip
{
$trip = new self;
$trip->apply(new TripWasRegistered($identity, $name));
$trip->apply(new RouteWasPlanned($identity, 1, $name));

return $trip;
}
и это https://github.com/qandidate-labs/broadway/blob/master/src/Broadway/EventSourcing/EventSourcedAggregateRoot.php
Сущность наследуется от этого класса. Под обработкой понимаю вызов apply и далее что он внутри вызывает. Не хочу копить события внутри $uncommittedEvents.

Это не нравится, связано с предыдущим, хочется события получать сразу после вывода метода, хотя нужно реально попробовать.

Тогда нужно возвращать массив событий, их может быть несколько.
Да, если их несколько.

$this->userRepository->save($user);
Это уже не нужно будет, можно же события проиграть и подписчики-репозитории всё сохранят. Нет?
 

Вурдалак

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

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

Юрий Быков

Новичок
Тогда вёрнемся к репозиторию, что в нём? Изначально меня это интересовало.
PHP: public funtion save(Trip $tip): void
{
// $trip->getEvents()
}

private onTripWasRegistered(...): void
{
// INSERT INTO trips VALUES ($tripId, $tripName, ...);
}

private onRouteWasPlanned(...): void
{
// INSERT INTO routes VALUES ($routeId, $tripId, $routeName);
}
Это разве не он с методами на события?
 

Юрий Быков

Новичок
@Вурдалак, а, ну, тогда понятно почему тебе нужно массив ивентов хранить в сущности. Особой разницы тогда не будет дернуть через dispatch, других подписчиков быть не должно, ну, или завести отдельный тип событий именно для персистентности, смотреть по реальному проекту. Только потребуется исполнять события в том порядке, в котором они появились, иначе неконсистентность можно словить.
 
Сверху