Снова немного DDD, моё решение пары проблем

Вурдалак

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

В примере с Client я бы сделал акцент не только на том, что мы можем ввести сущность в невалидное состояние. Здесь важен тот момент, что свойства сущности в принципе не важны. Лучше всего это заметно на ES-моделях, когда мы воздействуем на модель при помощи бизнес-методов и получаем какой-то отклик в виде событий. Модель — черный ящик, последовательность полученных событий — это и есть «поведение». То, каким образом этот список событий мы получили — это детали реализации. Я на практике сталкивался с тем, когда приходилось менять внутри одно поле со статусом на несколько разных флагов — я менял модель, но при этом внешний API — события — оставались теми же, read model тоже никак не менялась.

Твой пример с Client выглядит неубедительно отчасти из-за того, что там вовсе нет логики. Ты просто говоришь — записывайте данные вот так, а не через сеттеры. Туда просто попадают обычные данные: имя, менеджер, адрес, etc. Нет бизнес-методов. Нет даже простых assertion'ов. Выглядит так, словно ты просто перегруппировал данные и всё. Я даже предпочитаю не делать свойства в модели для тех данных, которые никак не влияют на логику. Типа имени. Оно просто проваливается в событие и попадает в read model.

Касаемо ClientBuilder. Неясно почему именно builder. Там что, какие-то поля опциональны? Твой код там равнозначен фабрике:
PHP:
$client = $builder->setId($id)
    ->setName($name)
    ->setGeneralManagerId($generalManager)
    ->setCorporateForm($corporateForm)
    ->setAddress($address)
    ->buildClient();
<=>
PHP:
$client = $factory->createClient(
    $name,
    $generalManager,
    $corporateForm,
    $address
);
Тут хотя бы ясно, что все поля обязательны.

Но иногда возникает необходимость работать с уже существующей структурой БД, в которой таблица хранящая соответствующие данные имеет для идентификатора автоинкрементное поле.
А что тебе мешает перевести существующую таблицу на sequence? Берешь, увеличиваешь на продакшен таблице AUTO_INCREMENT на 100500, создаешь sequence с id, который был максимальный на момент увеличения. Тут главное, чтобы на время тестов твой sequence не «догнал» AUTO_INCREMENT с прода. Потом после деплоя увеличиваешь sequence как MAX(id) + 100500. Я так не одну legacy-таблицу переводил, потому что с AUTO_INCREMENT работать невозможно.

P.S. У меня нет аккаунта на хабре, но вот тот чувак с ником Delphinum явно пытается рассказать старую песню о DRY для валидации, говоря про пользователей и админов. Мол, дублирование в модели и в контроллере/форме/точке доступа, как же так!!11 Тут важно таким людям объяснять различие этих валидацией, они с разных уровней, в определенном смысле — из разных контекстов. И ничего плохого в таком дублировании нет.
 

Sufir

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

Касаемо ClientBuilder. Неясно почему именно builder.
Ну, во-первых потенциально некоторые могут быть опциональными, или стать такими при изменении требований. А во-вторых избавляет от портянки аргументов в одном методе за счет fluent interface. Кто-то здесь на форуме плевался, что мол, пусть лучше будет потенциально невалидный объект, но я его сеттерами построю, чем пол десятка и больше аргументов, а не так давно то же самое и на работе услышал. Вот тут и сеттеры есть и сущность недоделанная не появится. И в общем-то резонно, читабельность повышается, хотя становится не очевидно какие обязательны - диалектика.
Опять же, если несколько правил создания сущности будет, то будет несколько именованных конструкторов их выражающие, а билдер и один может быть. Первая мысль именно о фабрике была.

По поводу тем согласен, с одной стороны хотелось узким вопросом ограничить, с другой что-бы понятен контекст был. Вероятно несколько сумбурно, в школе за сочинения я больше тройки редко получал, «чукча не писатель, чукча читатель». Там в коментах вообще в другие дебри полезли.

Тут важно таким людям объяснять различие этих валидацией, они с разных уровней, в определенном смысле — из разных контекстов. И ничего плохого в таком дублировании нет.
Хотел, но что-то утомил он меня к концу уже, нет сейчас мотивации, а потом забуду. Там можно комментировать аккаунтами readonly сейчас, просто такие комменты автор подтвердить должен, напиши. Да и собственно замечания тоже, хоть один коммент напрямую по делу будет, а не по касательной.
 
Последнее редактирование:

MiksIr

miksir@home:~$
А во-вторых избавляет от портянки аргументов в одном методе за счет fluent interface.
Заменяя ловлю статической ошибки (несоответствие аргументов конструктора) на ловлю рантайм ошибки. Ну это как перейти от статической типизации к динамической.

Кстати, можете скинуть пример кода, в котором есть авторизация определенных действий в домене
 

Вурдалак

Продвинутый новичок
Ну, во-первых потенциально некоторые могут быть опциональными, или стать такими при изменении требований.
Ээ, ну, = null поставишь. Или другой named constructor сделаешь.

А во-вторых избавляет от портянки аргументов в одном методе за счет fluent interface.
А что плохого в «портянке аргументов», если уж её не избежать? Не очень красиво выглядит?

Меня немного удивляет попытка сэкономить пару минут, когда само моделирование иногда занимает не один день. Сидишь, думаешь о высоких материях, use cases, правильные события, нейминг. И тут бац — буду писать «setFoo()» вместо «$foo,», потому что портянка, так выйдет на полминуты быстрее.

В PhpStorm, к слову, можно показывать имя вводимого аргумента: https://www.jetbrains.com/help/phpstorm/2016.3/viewing-method-parameter-information.html
 

Sufir

Я не волшебник, я только учусь
Не очень красиво выглядит?
Да, не очень, впрочем лично меня бы это не сильно смутило, но это компромисс в том случае когда у человека непереносимость большого количества атрибутов. Как пример - http://phpclub.ru/talk/threads/infrastructure-vs-domain-или-как-реализовать-правила-предметной-области.81226/#post-735744
 

Вурдалак

Продвинутый новичок
Да, не очень, впрочем лично меня бы это не сильно смутило, но это компромисс в том случае когда у человека непереносимость большого количества атрибутов. Как пример - http://phpclub.ru/talk/threads/infrastructure-vs-domain-или-как-реализовать-правила-предметной-области.81226/#post-735744
По-моему, это просто мнение человека, который недостаточно разобрался в ситуации, но кричит громче всех. Зачем тратить своё время на некомпетентных людей?

new User(), а уж тем более Client::register() должен вызываться где-то в одном месте — в command handler, где происходит регистрация. О каких там «по всему коду» идёт речь — загадка.

Далее, а что, если ты добавил новое свойство, то сеттер типа вызывать во всех эти «по всему коду» не нужно? Типа, и так сойдёт?

И вообще, если у тебя полно nullable-полей, и ты пишешь типа null, null, $foo, null, то это намёк на то, что происходит попытка использования одного метода под разные use case'ы. Сделай несколько разных методов.

Когда ты создаёшь объект, тебе должно быть необходимо и достаточно передать ТОЛЬКО данные, уникальным образом его идентифицирующие.
— wat? Мне всегда казалось, что в конструктор нужно передать те данные, которые необходимы для корректного конструирования объекта. Если какое-то поле обязательное, а его не передают, то идите в жопу, у вас невалидные объекты, 3D точка без Y, Z координат.

Всю остальную требуху - в сеттеры, с валидацией.
— сразу чувствуется знаток сеттеров с требухой и валидацией.
 
Последнее редактирование:

fixxxer

К.О.
Партнер клуба
А кстати, _где_ кто генерирует ID? Я раньше фабрики-аллокаторы делал, а щас просто сую метод allocateId() в репозиторий, не очень эстетично, зато практично
 

Sufir

Я не волшебник, я только учусь
Кстати, можете скинуть пример кода, в котором есть авторизация определенных действий в домене
О какой авторизации речь, проверке, может ли выполнить пользователь данную команду или запрос? У меня проект был на Yii2, я использовал их RBAC на уровне приложения, в домен ничего не тащил. Впрочем там система достаточно простая статичная была.

А кстати, _где_ кто генерирует ID? Я раньше фабрики-аллокаторы делал, а щас просто сую метод allocateId() в репозиторий, не очень эстетично, зато практично
Я обычно выделяю интерфейс, вроде IdentityGenerator, а реализацию тоже к репозиторию цеплял.
PHP:
$generator = $container->get(IdentityGenerator::class);
$generator->allocateId();
Практично и достаточно эстетично. В клиентском коде же не видно, а в репозитории удобно, там для этого всё есть. Небольшое отступление от SRP, но действительно удобно.


Так что всё-таки с билдером? Я не правильно его применил или проблема в самом шаблоне?

Пример из википедии: https://ru.wikipedia.org/wiki/Строитель_(шаблон_проектирования)#PHP5 Если я заказал пиццу с курицей и её там не окажется, я буду сильно расстроен.
Пример на гитхабе: https://github.com/domnikl/DesignPatternsPHP/blob/master/Creational/Builder/Director.php Если мы забудем навесить двери или движок - это проблема.
Взять любой SQL QB, так-же существует масса способов построить кривой запрос.
 

fixxxer

К.О.
Партнер клуба
Насчет интерфейса IdentityGenerator не понял, не вижу смысла в таком при отсутствии дженериков. Делать интерфейсы типа UserIdentityGenerator на каждую сущность, у которой может быть id? А зачем?
 

fixxxer

К.О.
Партнер клуба
С билдером не так то, что непонятно, нахрена он нужен:)

В чем проблема с 10 аргументами конструктора? Если они все нужны - ну и ок, если много опциональных, то, как правило, есть небольшой набор кейсов, можно сделать несколько конструкторов, а если это не так, то, возможно, модель несколько анемична.
 

Sufir

Я не волшебник, я только учусь
Насчет интерфейса IdentityGenerator не понял, не вижу смысла в таком при отсутствии дженериков. Делать интерфейсы типа UserIdentityGenerator на каждую сущность, у которой может быть id? А зачем?
Можно и не делать, я делал, не в падлу, дело минутное. Это ж ISP. С дженериками было бы приятнее конечно. И в теоретическом случае изменения способа генерации уже несколько странновато будет помещать в репозиторий генерацию UUID к примеру.
 

Вурдалак

Продвинутый новичок
А кстати, _где_ кто генерирует ID? Я раньше фабрики-аллокаторы делал, а щас просто сую метод allocateId() в репозиторий, не очень эстетично, зато практично
Я генерирую до вызова команды:
PHP:
$id = $this->sequence->generate('client');
$this->bus->handle(new RegisterClient($id, ...));
$client = $this->clientQueryService->find($id);
В противном случае встанет вопрос как получить $id после вызова команды.
 

MiksIr

miksir@home:~$
О какой авторизации речь, проверке, может ли выполнить пользователь данную команду или запрос? У меня проект был на Yii2, я использовал их RBAC на уровне приложения, в домен ничего не тащил. Впрочем там система достаточно простая статичная была.
Ну да. Т.е. есть требование, допустим, что только менеджер может регистрировать нового клиента. Вроде, должно быть в домене. Значит нужно вводить понятие текущего пользователя/контекста безопасности, и как-то проверять. Или делать отдельный сервис, или спрашивать сущность. Вот такие примеры хочется, кто как делает. Кое-что накопал, но пока мало.
 

Вурдалак

Продвинутый новичок
Ну да. Т.е. есть требование, допустим, что только менеджер может регистрировать нового клиента. Вроде, должно быть в домене. Значит нужно вводить понятие текущего пользователя/контекста безопасности, и как-то проверять. Или делать отдельный сервис, или спрашивать сущность. Вот такие примеры хочется, кто как делает. Кое-что накопал, но пока мало.
Домен домену рознь. Есть понятие контекстов. Есть контекст твоей модели, а есть контекст прав доступа. И контекст прав доступа можно реализовать в виде штатных средств фреймворка. Подобные домены обычно называют generic, т.е. они могут быть в виде библиотеки и переиспользоваться в разных проектах. Обычно нет нужды пилить свои модели а-ля Role. Но нужно сконфигурировать, да.
 

Andreika

"PHP for nubies" reader
Как у человека, не прочитавшего книжки по ссылкам у меня возникли вопросы
1. Entity обычно употребляется рядом со словом Repository и есть даже мнение, что пустые Entity должны приходить оттуда же (но не суть). В каком месте размещается этот Builder? Он нужен для инициализации новых (не сохраненных в хранилище) объектов, этот билдер создает сущность внутри репы или сущности из хранилища получаются не из репозитория, а именно из билдера?
2. В случае, если репозиторий все таки используется, то почему сущность не может "заполнить" сама себя теми же "кошерными" бизнес-методами из билдера?
3. Ну и в ответе Delphinum про валидацию и разные бизнес-правила вы написали
===========================================
Именованные конструкторы. Более правильным решением будет не использовать дефолтный конструктор совсем, а пользоваться для этого именованными конструкторами, которые так же будут явным образом отображать единый язык и предметную область.
Client::register(...): Client;
Когда появляется новое бизнес-правило, которое требует регистрацию клиента без менеджера вы вводите ещё один именованный конструктор, который явно выражает это требование.
// имя должно характеризовать бизнес требование и соответствовать единому языку
Client::registerWithoutClient(...): Client;

================= и ниже по тексту ================
Вы просто валите всё в кучу.
// первое бизнес-правило - регистрация клиента (тут менеджер не обязателен)
Client:register($id, ..., $manager = null);
=============================================

Собственно клиент у нас раньше создавался сначала сеттерами, потом через 8 параметров конструктора, потом через взятый из контейнера билдер и тут внезапно статический "именованный конструктор". Откуда он взялся и где используется? В билдере, вместо него или еще где?
 

AnrDaemon

Продвинутый новичок
"Вместо, милая, вместо." (q) анекдот
PHP:
class Entity
{
  private function __construct() {}

  static function create($param1, $param2, ...): Entity
  {
    $new = new static();
    ...
    return $new;
  }

  static function createSpecial($paramX, $paramY, ...): Entity
  {
    $new = new static();
    ...
    return $new;
  }
}
 

Andreika

"PHP for nubies" reader
не, про "конструкторы" и их реализацию все было понятно. не понятно про анекдот:
если действительно вместо, как улучшенный способ решения задачи? тогда почему статья закончилась на билдере.
если вместо, но это плохой вариант, то зачем он всплыл и куда девать валидацию в билдере?
а может даже и не вместо, а совместно - где-то в недрах билдера вызывается сей конструктор. тогда как выбирается, какой из конструкторов(методов) вызывать?
 

Вурдалак

Продвинутый новичок
Я подозреваю, что вся эта затея с билдером имеет непосредственное отношение к решению проблемы с AUTO_INCREMENT. Не будь такой проблемы, скорее всего не было бы и билдера :) А так, билдеру ничего не мешает иметь те же несколько методов build или какой-то дополнительный аргумент, чтобы решить какой из конструкторов вызвать внутри.
 
Сверху