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

В данной статье описываются две такие проблемы, и рассматривается способ их решения. Так же статья подойдет как введение в проектирование сущностей. Для понимания материала понадобится базовое представление о предметно-ориентированном проектировании.

Итак, мы изучили предметную область, сформировали единый язык, выделили ограниченные контексты и определились с требованиями [2]. Всё это выходит за рамки данной статьи, тут мы попробуем решить конкретные узкие проблемы:

  1. Создание и обеспечение консистентности сложных объектов-сущностей.
  2. Создание объектов-сущностей с генерацией идентификатора по автоинкрементному полю базы данных.

Введение


У нас есть клиент, который должен быть смоделирован как сущность (Entity) [2]. С точки зрения бизнеса у каждого клиента обязательно есть:

  • имя или наименование;
  • организационная форма (физ. лицо, ИП, ООО, АО и т.д.);
  • главный менеджер (один из менеджеров, закрепляется за клиентом);
  • информация о фактическом адресе;
  • информация о юридическом адресе.

А так же может быть всевозможная дополнительная информация.

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

namespace Domain;

final class Client
{
    public function getId(): int;

    public function setId($id): void;

    public function setCorporateForm($corporateForm): void;

    public function setName($name): void;

    public function setGeneralManager(Manager $manager): void;

    public function setCountry($country): void;

    public function setCity($city): void;

    public function setStreet($street): void;

    public function setSubway($subway): void;
}

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

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

$client = new Client();
// В данный момент клиент у нас уже находится в не консистентном состоянии
// Если мы хотим запросить его идентификатор, то получим ошибку, т.к. он ещё не установлен
$client->getId();
// Или мы можем сохранить (попытаться) не валидного клиента, у которого не установлены обязательные свойства
$repository->save($client);

Создание и обеспечение консистентности сложных объектов-сущностей.


Для начала попробуем решить проблему консистентности. Для этого уберем из класса сеттеры, а все обязательные параметры будем запрашивать в конструкторе [4]. Таким образом, объект будет всегда валиден, может использоваться сразу после создания и обеспечивается полноценная инкапсуляция предотвращающая возможность приведения клиента в неконсистентное состояние. Теперь наш класс выглядит следующим образом.

namespace Domain;

final class Client
{
    public function __construct(
        $id,
        $corporateForm,
        $name,
        $generalManager,
        $country,
        $city,
        $street,
        $subway = null
    );

    public function getId(): int;
}

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

Что можно с этим сделать? Простое и очевидное решение — сгруппировать логически связанные параметры в объектах-значениях (Value Object) [3].

namespace Domain;

final class Address
{  
    public function __construct($country, $city, $street, $subway = null);
}

namespace Domain;

final class Client
{
    public function __construct(
        int $id,
        string $name,
        Enum $corporateForm,
        Manager $generalManager,
        Address $address
    );

    public function getId(): int;
}

Выглядит гораздо лучше, но параметров всё ещё довольно много, особенно это не удобно, если часть из них скалярные. Решение — шаблон Строитель (Builder) [5].

namespace Application;

interface ClientBuilder
{
    public function buildClient(): Client;

    public function setId($id): ClientBuilder;

    public function setCorporateForm($corporateForm): ClientBuilder;

    public function setName($name): ClientBuilder;

    public function setGeneralManager(Manager $generalManager): ClientBuilder;

    public function setAddress(Address $address): ClientBuilder;
}

$client = $builder->setId($id)
    ->setName($name)
    ->setGeneralManagerId($generalManager)
    ->setCorporateForm($corporateForm)
    ->setAddress($address)
    ->buildClient();

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

Создание объектов-сущностей с генерацией идентификатора по автоинкрементному полю базы данных.


У проектируемого класса обязательно должен быть уникальный идентификатор, т.к. основной отличительной чертой сущностей является индивидуальность. Объект может значительно изменяться с течением времени, так что ни одно из его свойств не будет равным тому, что было вначале. В то же время все или большинство свойств объекта могут совпадать со свойствами другого объекта, но это будут разные объекты. Именно уникальный идентификатор дает возможность различать каждый объект не зависимо от его текущего состояния [1].

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

Cгенерировать, например, UUID или запросить у базы данных очередное значение последовательности не составляет никаких трудностей. Но иногда возникает необходимость работать с уже существующей структурой БД, в которой таблица хранящая соответствующие данные имеет для идентификатора автоинкрементное поле.

В таком случае крайне затруднительно и ненадежно получать очередной уникальный идентификатор без сохранения сущности, а значит невозможно создать объект в полностью консистентном состоянии.

И снова нам поможет в этом шаблон Строитель, который мы можем реализовать следующим образом.

namespace Infrastructure;

final class MySqlClientBuilder implements ClientBuilder
{
    private $connection;

    public function __construct(Connection $connection);

    public function buildClient()
    {
        $this->connection
            ->insert('clients_table', [
                $this->name,
                $this->corporateForm,
                $this->generalManager->getId(),
                $this->address
            ]);
        
        $id = $this->connection->lastInsertId();
        
        return new Client(
            $id,
            $this->name,
            $this->corporateForm,
            $this->generalManager,
            $this->address
        );
    }
}

Таким образом мы сначала добавляем соответствующую запись в базу данных, после чего получаем её идентификатор и создаем объект. Клиенту об этом знать не обязательно и при изменении способа хранения или генерации идентификаторов вам понадобится только заменить реализацию Строителя в вашем контейнере зависимостей.

$builder = $container->get(ClientBuilder::class);

$client = $builder->setId($id)
    ->setName($name)
    ->setGeneralManagerId($generalManager)
    ->setCorporateForm($corporateForm)
    ->setAddress($address)
    ->buildClient();

$repository->save($client);

$client->getId();

Благодарю за внимание!

P.S.:


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

Опыт разработки у меня относительно не большой — четыре года, DDD применял пока только на одном проекте.

Буду благодарен за отзывы и конструктивные замечания.


Ссылки:


  1. Эванс Э., «Предметно-ориентированное проектирование (DDD). Структуризация сложных программных систем.»
  2. Вернон В., «Реализация методов предметно-ориентированного проектирования.»
  3. М. Фаулер, Value Object
  4. М. Фаулер, Constructor Initialization
  5. Э. Гамма, Р. Хелм, Р. Джонсон, Д. Влиссидс, «Приёмы объектно-ориентированного проектирования. Паттерны проектирования.»
Поделиться с друзьями
-->

Комментарии (204)


  1. apelserg
    07.02.2017 15:57

    Проектирование сущностей начинается с составления ER-модели. Предварительно желательно описать бизнес-процессы. Для увязки целостности процессов используется язык UML. Для этого существуют соответствующие CASE-системы (например BPWin, ERWin, Rational Rose, Oracle Designer).


    1. Sufir
      07.02.2017 16:47
      +1

      UML-проектирование — это о другом. Вероятно не совсем удачно подобрано название статьи. Тут речь о проектировании классов представляющих в коде сущности предметной-области, собственно акцент на создание объектов, а поведение и отношения остаются за рамками.

      Изучение предметной области, формирование единого языка, выделение ограниченных контекстов, документирование, проектирование, описание и формализация в виде диаграмм или каким-то ещё способом отношений и бизнес-процессов и ещё много чего, всё это невозможно объять одной статьей, даже одной книгой если только в общем виде. О диаграммах, схемах и документировании в книге Эванса так же есть отличная глава. Как я указал вначале, всё это выходит за рамки данной статьи. Тут я просто поделился решением пары конкретных проблем, с которыми сам столкнулся в своём небольшом опыте.


      1. Toshiro
        08.02.2017 11:27
        -3

        UML-проектирование, это именно об этом.


        https://ru.m.wikipedia.org/wiki/Диаграмма_классов


        То что тема обширная — не оправдание тому, чтобы в статье хаотично все смешать в одну кашу. Есть проектирование и есть реализация. Проектирование — ERM/UML/IDEF/ARIS/итд., реализация — SQL/ORM/%language_name%/итд.


        Если ваша статья про проектирование, то где диаграммы? Если про реализацию… то где диаграммы, которые вы реализуете?)) Не надо приучать новичков к плохому) Всегда начинайте любое проектирование с хоть какой-нибудь схемы)


        1. mayorovp
          08.02.2017 11:50
          +1

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


        1. Delphinum
          08.02.2017 12:12
          +2

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


        1. Sufir
          08.02.2017 12:16
          +2

          Диаграммы классов я не рисую, занятие в большинстве случаев бесполезное. Слишком трудоёмко их поддерживать в актуальном состоянии, а с применением DDD код сам по себе является прекрасной документацией. Обычно хватает use case diagram, activity diagram и/или упомянутой ER-модели. В общем тут по ситуации.
          Но несомненно, всегда лучше начать с того, что бы немного порисовать, как я и сказал, к данному моменту вы «изучили предметную область, сформировали единый язык, выделили ограниченные контексты и определились с требованиями», написали проектную документацию, в соответствии с требованиями и принятым регламентом в вашей компании, почистили зубы и возможно много чего ещё сделали. Здесь речь о проектировании класса, это я делаю без схем.

          Ну, и как я уже писал выше:

          О диаграммах, схемах и документировании в книге Эванса так же есть отличная глава.
          Рекомендую, там подробнее.

          Проектирование — ERM/UML/IDEF/ARIS/итд.

          Я к «и т.д.» отношу ещё и объектно-ориентированное проектирование, предметно-ориентированное проектирование, а SQL/ORM/%language_name% — это детали, ООП и DDD от них принципиально не зависят, хотя и приходится считаться с техническими ограничениями одно из которых в статье разобрано.

          Так вот разработка предварительных проектных решений вместе с разработкой документации на информационную систему относится к стадии реализации проекта в соответствии с ГОСТом (если не ошибаюсь 34.601-90, могу не точно помнить) и относятся к этапу технического и рабочего проектирования. Это если уж совсем в формализм.


    1. sayber
      07.02.2017 19:56
      +2

      Не всегда.
      Есть бизнес подход от обратного. Когда заранее не известно что должно быть.
      Тогда мы применяем DDD.


  1. Delphinum
    07.02.2017 16:53
    +1

    В любой момент, при таком подходе, можно создать объект в неконсистентном состоянии или нарушить бизнес-логику

    Состояние сущности может быть не валидно для одной цели, и валидно для другой. Тут мы упираемся в «Контекстуальную валидацию».

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

    А что мешает программисту создать клиента с невалидным состоянием не вызывав пару методов Строителя:
    $client = $builder->setId($id)
        ->setName($name)
        ->setGeneralManagerId($generalManager)
        ->buildClient();
    


    Таким образом мы сначала добавляем соответствующую запись в базу данных, после чего получаем её идентификатор и создаем объект.

    Как вариант, но что делать с этими созданными объектами в случае возникновения ошибки в системе? Может проще не использовать генерацию PK на стороне СУБД, а воспользоваться генераторами UUID?


    1. Sufir
      07.02.2017 17:24
      +1

      Состояние сущности может быть не валидно для одной цели, и валидно для другой. Тут мы упираемся в «Контекстуальную валидацию».

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

      Если это одна сущность, то её контексты зашиты у ней внутри. В рамках же разных ограниченных контекстов проектируются разные объекты. Один объект, являющийся в данном контексте сущностью, в другом даже может быть представлен как объект-значение. В общем это тема отдельная и достаточно масштабная, подробнее у Эванса, Вернона и Фаулера.

      А что мешает программисту создать клиента с невалидным состоянием не вызвав пару методов Строителя

      Во-первых, ваша реализация строителя может (и должна) выбросить RuntimeException или скорее LogicException
      Во-вторых, если в Строителе вы забыли проверить, то ваш скрипт упадет с TypeError или ArgumentCountError.
      В общем это уже ошибка разработчика и должна быть разрешена разработчиком. Строитель просто дает возможность создавать сложный объект более гибко и прозрачно.

      Может проще не использовать генерацию PK на стороне СУБД, а воспользоваться генераторами UUID?

      Это прекрасный вариант и когда это возможно я так делаю, мне нравится UUID, так же я запрашивал идентификаторы у последовательностей Postges, но речь о конкретном случае. Структура БД уже существует и изменить её невозможно, я нашел вот такое решение и оно неплохо вписалось.

      А если появится возможность перейти на MySQL или использовать UUID, то без проблем я только заменю реализации Строителей.


      1. Delphinum
        07.02.2017 17:39

        Это не верно, сущность может быть либо валидна либо нет

        Представим сущность Клиент с тем же набором свойств, что предлагаете вы. Вы всем пользователям отказываете в создании Клиента без указания GeneralManager. Через n времени пользователь обращается к вам с просьбой дать возможность создания Клиента без указания GeneralManager с целью регистрации всех Клиентов, но запретить оформлять на этого Клиента заказы, при отсутствии у него GeneralManager. Как вы будете решать эту задачу?

        Во-вторых, если в Строителе вы забыли проверить

        Что проверить?


        1. Sufir
          07.02.2017 18:07

          Что проверить?

          Очевидно наличие необходимых для создания объекта параметров. А какого поведения вы ожидаете, например, от QueryBuilder'а, если не укажите таблицу? Он может вам построить кривой SQL без указания FROM и ваш скрипт будет падать при попытке его выполнить, либо в момент построения билдер проверит наличие обязательных параметров и выбросит исключение. Третьего я здесь не вижу.

          Представим сущность Клиент с тем же набором свойств, что предлагаете вы. Вы всем пользователям отказываете в создании Клиента без указания GeneralManager. Через n времени пользователь обращается к вам с просьбой дать возможность создания Клиента без указания GeneralManager с целью регистрации всех Клиентов, но запретить оформлять на этого Клиента заказы, при отсутствии у него GeneralManager. Как вы будете решать эту задачу?

          В соответствии с DDD, бизнес-правила явным образом выражены в коде, есть явное бизнес-требование: «клиент не может быть без менеджера, при регистрации за ним обязательно закрепляется менеджер». Изменение бизнес требований требует внесения изменений в код.

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

          Именованные конструкторы. Более правильным решением будет не использовать дефолтный конструктор совсем, а пользоваться для этого именованными конструкторами, которые так же будут явным образом отображать единый язык и предметную область.
          Client::register(...): Client;
          

          Когда появляется новое бизнес-правило, которое требует регистрацию клиента без менеджера вы вводите ещё один именованный конструктор, который явно выражает это требование.
          // имя должно характеризовать бизнес требование и соответствовать единому языку
          Client::registerWithoutClient(...): Client;
          


          1. Delphinum
            07.02.2017 18:17
            +1

            А какого поведения вы ожидаете, например, от QueryBuilder'а, если не укажите таблицу?

            А откуда Строителю известно, для каких целей я создаю Сущность? Другими словами, что если мне нужен SQL именно без FROM?
            Когда появляется новое бизнес-правило, которое требует регистрацию клиента без менеджера вы вводите ещё один именованный конструктор, который явно выражает это требование

            А если таких требований десятки, будет десяток конструкторов? А как же Строители? А как пользователь узнает о проблемах, приведших к отказу в оформлении заказа на данного Клиента (без GeneralManager)?

            Давайте я приведу другой пример: у вас есть сущность Клиента, и для оформления Заказа этому клиенту необходимо наличие у него как Адреса, так и ОтветственногоМенеджера. При этом сохранить клиента можно без этих сведений, а вот для отгрузки товара по Заказу достаточно знать только Адрес Клиента. Вы предлагаете создать 3 конструктора вида: register, registerWithoutManager, registerFull — или как?


            1. Sufir
              07.02.2017 18:35

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

              А если таких требований десятки, будет десяток конструкторов?

              Разумеется, если есть требование, то оно должно быть выражено в коде, в этом и смысл. Правда десятки бизнес-правил создания сущности мне представить сложно. Я поделился конкретным опытом и вполне возможна ситуация где он окажется неэффективным или вовсе неприемлемым.

              Ну, вот я тут вижу три размытых бизнес-требования. И при чем здесь конструкторы мне не понятно. Вы собираетесь в конструкторе осуществлять «оформления Заказа» и «отгрузки товара»?

              1. «сохранить клиента можно без этих сведений»
              Значит Client::register(...): Client; будет без менеджера и адреса или они будут необязательными.

              2. «для оформления Заказа этому клиенту необходимо наличие у него как Адреса, так и ОтветственногоМенеджера»
              3. «для отгрузки товара по Заказу достаточно знать только Адрес Клиента»

              А вот эти два бизнес-правила к регистрации клиента (а значит и к конструкторам) уже не имеют никакого отношения.


              1. Delphinum
                07.02.2017 18:39

                Речь не о регистрации Клиента, а о валидации состояния сущности. Для регистрации Клиента требуется одна валидация, для создания Заказа требуется другая валидация (возможно связанная с первой). Я об этом сказал здесь:

                Состояние сущности может быть не валидно для одной цели, и валидно для другой. Тут мы упираемся в «Контекстуальную валидацию».


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

                Будет ли сущность Клиент валидна, без Адреса и ОтветственногоМенеджера? Для сохранения вполне, но для создания Заказа — нет. Отталкиваясь от ваших рассуждений, такая ситуация невозможна. Парадокс )


                1. Sufir
                  07.02.2017 18:52

                  Да вы вообще не о том сейчас.

                  Для регистрации Клиента требуется одна валидация, для создания Заказа требуется другая валидация

                  Это два разных бизнес-процесса связанных с одной сущностью.

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

                  Есть конкретное бизнес-требование "клиент может быть зарегистрирован без менеджера". Не важно, что я могу его сохранить в БД и вообще что храню в БД, в бизнесе нет никаких «сохранений» и «баз данных». Есть «регистрация клиента» — конкретное оговоренное документированное требование, которое позволяет регистрировать клиентов без менеджера.

                  Есть другое бизнес-требование "для оформления Заказа у Клиента должен быть Менеджер", вот там вы и будете проверять наличие менеджера и обрабатывать эту ситуацию так как оговорено в соответствии с данным бизнес-требованием.


                  1. Delphinum
                    07.02.2017 18:58

                    Видимо я плохо излагаю свою идею. Ну ладно, не может так не может )


                    1. Sufir
                      07.02.2017 19:10

                      Вы просто валите всё в кучу.

                      // первое бизнес-правило - регистрация клиента (тут менеджер не обязателен)
                      Client:register($id, ..., $manager = null);
                      
                      // второе бизнес-правило - оформление заказа (менеджер обязателен)
                      Order::checkout($client, ...);
                      
                      class Order {
                          function checkout($client, ...)
                          {
                              if (!$client->hasManager()) {
                                  throw new ClientHasntManagerException();
                              }
                          }
                      }
                      


                      1. Delphinum
                        07.02.2017 19:13

                        А как ваше решение справится с кодом:

                        <?php
                        $client = new Client();
                        $client->setManager(new Manager));
                        
                        Order::checkout($client, ...);
                        


                        1. Sufir
                          07.02.2017 19:23

                          Что значит «справится»? Оно противоречит моему решению.
                          В соответствии с моим решением оно будет выглядеть:

                          $client = $clienBuilder->build();
                          
                          
                          $manager = $managerBuilder->build();
                          // или скорее
                          $manager = $managerRepository->get();
                          
                          
                          // метод выражает бизнес-действие "смена менеджера клиента"
                          $client->changeManager($manager));
                          
                          
                          Order::checkout($client, ...);
                          


                          Только это всё уже несколько за рамками статьи.


                          1. Delphinum
                            07.02.2017 19:31

                            Что значит «справится»? Оно противоречит моему решению

                            Вы предлагаете для каждого возможного варианта инстанциации класса реализовывать по одному статичному конструктору. Они должны, по вашей логике, возвращать экземпляры класса в определенном, как вы это называете, консистентном состоянии. Как вы предлагаете валидировать созданную таким образом сущность далее? К примеру:
                            <?php
                            $clientA = Client::register2($id, $name, $address);
                            $clientB = Client::register2($id, $name);
                            
                            Order::checkout($clientA); // Допустимо
                            Order::checkout($clientB); // недопустимо
                            

                            Как в данном случае вы реализуете Order::checkout?


                            1. Sufir
                              07.02.2017 19:43

                              Вы предлагаете для каждого возможного варианта инстанциации класса реализовывать по одному статичному конструктору.

                              Это вы уже нафантазировали.


                              1. Delphinum
                                07.02.2017 19:45

                                Именованные конструкторы. Более правильным решением будет не использовать дефолтный конструктор совсем, а пользоваться для этого именованными конструкторами, которые так же будут явным образом отображать единый язык и предметную область.

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

                                Правда?


                                1. Sufir
                                  07.02.2017 19:52

                                  Правда. Давайте по порядку, какое бизнес-правило выражает ваш метод «Client::register2»?

                                  Как проверять, соответствует ли клиент бизнес-требованию «оформить заказ можно только для Клиента у которого есть Менеджер» я показал выше:

                                  class Order {
                                      function checkout($client, ...)
                                      {
                                          if (!$client->hasManager()) {
                                              throw new ClientHasntManagerException();
                                          }
                                      }
                                  }
                                  

                                  Создание инстансов и тема статьи тут вообще не при чем.


                                  1. Delphinum
                                    07.02.2017 19:54

                                    Давайте по порядку, какое бизнес-правило выражает ваш метод «Client::register2»?

                                    Ну, предположим, первый регистратор регистрирует клиента с адресом (правило «клиент может быть зарегистрирован с адресом»), а второй регистрирует без адреса (правило «клиент может быть зарегистрирован без адреса»). Метод Order::checkout должен проверять возможность оформления заказа клиенту, но только с адресом.


                                    1. Sufir
                                      07.02.2017 19:58

                                      Ну, а в чём проблема проверить наличие адреса?

                                      class Order {
                                          function checkout($client, ...)
                                          {
                                              if (!$client->hasAddress()) {
                                                  throw new ClientHasntAddressException();
                                              }
                                      
                                              // ... оформляем Заказ
                                          }
                                      }
                                      


                                      1. Delphinum
                                        07.02.2017 19:59

                                        Ну как минимум в том, что требуется дублирование валидатора.


                                        1. Sufir
                                          08.02.2017 09:00

                                          Ознакомитесь с мнениями по данному вопросу ссылкам:
                                          http://gorodinski.com/blog/2012/05/19/validation-in-domain-driven-design-ddd/
                                          http://verraes.net/2015/02/form-command-model-validation/


                                          1. Delphinum
                                            08.02.2017 12:14

                                            Что же вы все меня футболите? У вас своего мнения нет по этому поводу? )


          1. reforms
            07.02.2017 18:22

            А какого поведения вы ожидаете, например, от QueryBuilder'а, если не укажите таблицу?

            Вот смотрите: я не указываю таблицу и...? Это валидный sql запрос для определнных СУБД: SELECT 1 на PostgreSql отрабатывает отлично.

            Я соглашусь с Delphinum — валидность / невалидность объекта определяет (возможно должна определять) бизнес логика, хотя в рамках конкретных задач бывает и так, что и сам объект успешно справляется с этой задачей, например, на уровне деклараций.


            1. Delphinum
              07.02.2017 18:26

              валидность / невалидность объекта определяет (возможно должна определять) бизнес логика

              Я сторонник мнения, что валидироваться объекты должны на основании контекста их использования. Так, состояние сущности Клиент может быть валидно для сохранения в базу, но не валидно для оформления заказа. Валидность так же может быть вложенной, на пример оформление заказа требует как валидности для сохранения в базу, так и дополнительных проверок.

              Реализуется это достаточно просто:
              <?php
              class Client{
                ...
              
                /**
                 * @return InvalidCollection
                 */
                public function isValidForPersist(){...}
              
              
                /**
                 * @return InvalidCollection
                 */
                public function isValidForCreateOrder(){...}
              }
              


              1. reforms
                07.02.2017 18:36
                +1

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


                1. Delphinum
                  07.02.2017 18:45

                  Я не работал с php, не знаю как принято в проектах на нем

                  Я работаю с php и, к сожалению, не встречал еще ни одного проекта, в котором применялась бы контекстуальная валидация. Да даже на github не нашел ничего, что заинтересовало бы в этом вопросе. Пришлось писать свое )


                  1. oxidmod
                    07.02.2017 19:37

                    В symfony есть группы валидаций (https://symfony.com/doc/current/validation/groups.html)
                    В Yii есть сценарии, на основании которых валидируется модели

                    зы. Компонент симфони для валидации самостоятельный и может юзатся без симфони


                    1. Delphinum
                      07.02.2017 19:43

                      Мне решение symfony показалось очень усложненным, так как предлагается смешивать механизм контекстной валидации с валидацией данных как таковой, я предпочитаю разделять это на пакеты. Думаю у Yii аналогичная проблема.


                      1. oxidmod
                        07.02.2017 21:59

                        В yii гораздо проще, но дело не в том. Не понравилось и ничего нету — разные вещи


                        1. Delphinum
                          07.02.2017 22:01

                          Вы видимо плохо прочитали мой комментарий. Я не утверждал, что решения нету, я сказал, что не встречал еще ни одного проекта с ней и не нашел ничего, что меня бы заинтересовало в качестве ее реализации.


                          1. oxidmod
                            07.02.2017 22:19

                            Да любой проект на симфони юзает компонент валидации.


                            1. Delphinum
                              07.02.2017 22:26

                              Я не работаю с симфони, а на гитхабе не встречал, если есть ссылка на готовый проект с использованием контекстной валидации, то буду благодарен.


                              1. oxidmod
                                08.02.2017 00:12

                                https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/config/storage-validation/orm.xml

                                Валидации сущности юзера в зависимости от контекста. Регистрация/редактирование профиля


                  1. oxidmod
                    08.02.2017 10:46

                    Кстати, глянул ваш код, по сути это почти тотже компопнент симфони, но:
                    1. с обрезанными возможностями. в конечном итоге валидация любого поля конечно сводится к is, но приятно иметь библиотеку готовых валидаторов мыла, минимальной/максимальной длины и прочее-прочее-прочее. На практике всего пару раз пришлось писать свой валидатор для повторного использования разными объектами и пару раз в виде колбека для разового использования по месту необходимости

                    2. компонент симфони позволяет не замусоривать код валидируемых объектов? вынося правила валидации в yml/xml конфиги.

                    Для меня этих преимуществ было достаточно чтобы с ним разобраться


                    1. Delphinum
                      08.02.2017 12:17

                      но приятно иметь библиотеку готовых валидаторов мыла, минимальной/максимальной длины и прочее-прочее-прочее

                      Приятно конечно. Мое решение позволяет подключить любой набор валидаторов, будь то symfony, zend или yii инкапсулировав их в предлагаемую мной структуру.

                      компонент симфони позволяет не замусоривать код валидируемых объектов?

                      Ну организовать фабрику, которая будет генерировать валидатор на основании yml/xml не проблема, если это необходимо в проекте.


                      1. oxidmod
                        08.02.2017 13:47
                        +1

                        Мое решение позволяет подключить любой набор валидаторов, будь то symfony, zend или yii инкапсулировав их в предлагаемую мной структуру.

                        ну собсвтенно симфониевский тоже, кроме того, что он обладает обширным набором из коробки

                        Ну организовать фабрику, которая будет генерировать валидатор на основании yml/xml не проблема, если это необходимо в проекте.

                        Ну вот допишите и получите тоже самое) Но я всеже предпочту уже готовый компонент


                        1. Delphinum
                          09.02.2017 01:10

                          Ну так я нисколько не настаиваю, код привел в качестве примера решения контекстуальной валидации.


              1. mayorovp
                08.02.2017 08:31

                В целом я с вами согласен, но вот методы я бы назвал по-другому. isValidForPersist — это, в моем понимании, просто isValid. Потому что объект, который нельзя сохранить в базу — заведомо непригоден ни для чего другого.


                А isValidForCreateOrder я бы сократил до canCreateOrder.


              1. Tramvai
                13.02.2017 21:22

                И чем это отличается от задумки автора? и у него и у вас есть валидация объекта. Разница в том, что вы запилили валидацию в сам обобьет, что само по себе не очень хороший вариант, потому как Клиент у вас теперь знает что-то про окружающий мир. Вы говорили, что проблема автора в копировании валидатора, это не проблема. Вынесите ваши 2 метода в отдельный сервис, и проводите валидацию в любом месте вашего приложение используя валидатор. Таким образом у вас валидация обьекта будет в зависимости от контекста. Я бы сказал, что вы с автором говорите об одном и том же, но используете разную архитектуру.


                1. Delphinum
                  13.02.2017 22:34

                  И чем это отличается от задумки автора?

                  Этим


              1. Fesor
                14.02.2017 00:16
                +1

                "Валидность для сохранения в базу" — это в целом бесполезная метрика. Любой объект который соблюдает свои инварианты по умолчанию "валиден для сохранения в базу". За соблюдением инвариантов должен следить сам объект.


                Что до примера с hasManager и подобными — абсолютно согласен, все это надо изолировать в сущности и сделать метод canCreateOrder или вообще возложить на юзера ответственность за формирования ордера.


                И да, сделав методы hasManager мы тем самым ломаем инкапсуляцию.


                1. Delphinum
                  14.02.2017 10:31

                  это в целом бесполезная метрика

                  Это одна из самых важных метрик. Вопрос скорее в том, следует ли ее выделять в отдельный валидатор или использовать метод вида canPersist, но валидировать однозначно стоит все с тем же механизмом формирования причин, по которым это самое сохранение не возможно.


                  1. VolCh
                    14.02.2017 13:06

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


                    1. Delphinum
                      15.02.2017 14:56

                      Совершенно верно. Отсюда можно сделать простой вывод — инвариант это частный случай контекстной валидации.


                      1. VolCh
                        15.02.2017 16:09

                        Не так. Инвариант ограничен (по хорошему) сверху пересечением всех контекстных валидаций.


                        1. Delphinum
                          15.02.2017 16:31

                          Согласен. Только вот имеет ли смысл это ограничение, ведь при добавлении или изменении всего множества валидаций, пересечение может изменяться. Не проще ли отказаться от инварианта как запрограммированной валидации?


                          1. VolCh
                            15.02.2017 16:49

                            Это уже от аналитиков зависит прежде всего.


                1. michael_vostrikov
                  14.02.2017 13:31

                  У меня есть мысль (правда не уверен насколько правильная), что все свойства бизнес-сущности должны быть доступны снаружи на чтение. Потому что мы их определили при анализе предметной области, при взгляде со стороны. Если бы это были детали реализации, мы бы их не обнаружили. Детали реализации — это, скажем, когда секретарша подписывает документы за шефа, потому что он ей разрешил. Официально это бумага, подписанная шефом. Мы об этом не узнаем, если только кто-нибудь из них сам не скажет.
                  Другое дело, что иногда одной сущности необязательно знать, что у другой сущности есть менеджер. Но это уже требования бизнес-логики для конкретного случая, а не требования архитектуры.


                  1. VolCh
                    14.02.2017 20:41

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


                  1. Fesor
                    14.02.2017 22:58

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

                    Сделайте read model и там используйте read-only публичные поля. Не вопрос. Ну там сериализации всякие и подобное. Хотя и это под вопросом (для этого придумали DTO). А для остальных случаев вам в целом нет смысла делать публичными поля — к ним никто не должен даже хотеть получать доступ. Ну то есть не нужны ни геттеры ни публичные поля. Стэйт остается в том объекте где он нужен. Если вам нужен стэйт другого объекта — значит что-то при декомпозиции пошло не так.


                    1. michael_vostrikov
                      15.02.2017 06:19

                      Так я как раз не про сериализацию, а про бизнес-логику.
                      Если у товара есть характеристика "Цвет корпуса: черный", то он доступен всем — и клиенту, и продавцу, и начальнику, который видит, что черные лучше продаются. Причем именно в виде бизнес-свойства бизнес-сущности "Товар". А как мы его храним — в виде EAV или обычного столбца в таблице — это уже детали реализации.
                      А потом начальник вводит правило — при покупке белого начислять покупателю 100 бонусов. Как-то нелогично передавать в оформление заказа DTO, а не сущность.


                      1. VolCh
                        15.02.2017 07:32

                        Как раз в заказ и различные документы логично. Скорее даже не DTO, а value object. Поскольку сущность на то и сущность, что изменяет состояние, а в документах, как правило, нужно состояние сущности на момент формирования. Вы же не хотите, чтобы при изменении, например, цены во всех электронных представлениях документов (у которых есть часто бумажные представления с подписями и мокрыми печатями) поменялись цены и суммы? Или при изменении фамилии у заказчика, чтобы в документах она поменялась?


                        1. michael_vostrikov
                          15.02.2017 09:23

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


                          1. VolCh
                            15.02.2017 09:46

                            Так мы получаем более сильную связанность сущностей заказа и клиента, особенно если из заказа дергать связи клиента. Из-за изменений в клиенте надо будет править заказы. Используя DTO или другие способы ограничения доступа извне к сущности (например вместо Customer в Order передавать PublicCustomerInterface, в котором только некоторые геттеры Customer перечислены, а то и реализуя в Customer OrderCustomerInterface описанный рядом с Order) мы снижаем связанность, при изменениях Customer ничего не придётся переписывать в Order.


                      1. Fesor
                        16.02.2017 09:09
                        -1

                        А потом начальник вводит правило — при покупке белого начислять покупателю 100 бонусов. Как-то нелогично передавать в оформление заказа DTO, а не сущность.

                        Попробуйте реализовать такое вот требование в рамках магазинчика. И сделать это без геттеров и вообще попыток добраться до стэйта. Если надо достать стэйт — значит либо логика должна быть в этом же объекте, либо мы должны объекту дать сервис какой-то которому объект делегирует логику и передаст нужные данные.


                        Все остальное — нарушение инкапсуляции. И всегда можно ее сохранять если достаточно подумать.


                        p.s. короч свойства объекта, как не крути, это детали объекта. Они должны быть спрятаны. То есть у доменных объектов уж точно не должно быть публичных пропертей.


                        1. VolCh
                          16.02.2017 10:14

                          А как быть с, например, персональными данными клиента, которые в принципе бизнес не интересуют, но которые должны быть в представлениях, прежде всего печатных формах по требованию госорганов? Да и вообще много может быть свойств у различных сущностей в текущих процессах предназначенных исключительно для представления. Потом, может быть, какого-то аналитика осенит, и эти свойства будут влиять на процессы, а пока просто «чтобы было доступно для показа и редактирования»


                          1. Fesor
                            16.02.2017 10:40

                            но которые должны быть в представлениях

                            Я уже говорил про представление. В них сущности пихать не нужно.


                            прежде всего печатных формах по требованию госорганов?

                            $report->print($pinter);

                            Да и вообще много может быть свойств у различных сущностей в текущих процессах предназначенных исключительно для представления.

                            Read Model, DTO. В целом нужно смотреть с позиции SRP. Ну и да, иногда проще фигануть геттеров пачку для представления.


                            1. VolCh
                              16.02.2017 10:57

                              Пускай не пихать сущности, а пихать DTO, но конструктор или фабрика DTO должны иметь доступ к свойствам сущности, чтобы передать их в представление. Есть, конечно, отражения, с которыми можно залезть, но как-то… А friendly модификатора в PHP нет.


                              1. Fesor
                                16.02.2017 11:34

                                Если мы посмотрим например на java а не на php, то там мы бы ассемблер DTO ложили бы в тот же пакет что и сущность. И тогда у ассемблера появился бы доступ к состоянию объекта. Еще как вариант — friend классы (для PHP есть RFC но когда ее примут и примут ли я не знаю). То есть смысл в том что если и экспоузить состояние, то явно не для всех а только для "доверенных лиц" скажем так.


                                То что в PHP это нельзя сделать нормально — ну печаль беда. Потому мне больше нравится идея делать выборки из базы на чтение прямо в DTO минуя мэппинги сущности и unit-of-work.


                                1. VolCh
                                  16.02.2017 12:48

                                  Если в PHP делать выборки из базы на чтение прямо в отдельные DTO, то обычно получается много тупой работы, а пользы не сильно больше чем от массивов. Как оптимизация запросов — вариант. Закладывать сразу в архитектуру — как и с любой оптимизацией :)


                        1. michael_vostrikov
                          16.02.2017 10:39
                          +1

                          То есть вы считаете, что товар никому не должен сообщать свой цвет? Вот вы бы как реализовали это требование без геттеров и публичных свойств?
                          Изменилась процедура оформления заказа — добавилось новое правило. В товарах и заказах ничего не поменялось. Если свойства доступны, изменения в коде будут повторять изменения в реальности. Добавляем в процедуру оформления заказа новое правило с проверкой (хардкодом или отдельным классом в цепочке правил, не суть), и на этом всё. А со скрытым состоянием придется его открывать, причем специально для конкретного правила, то есть основываясь на деталях реализации другого объекта.
                          Я наверно соглашусь, что можно передавать не саму сущность, а прокси, который содержит только методы для чтения свойств. Но чтобы вообще всё скрывать, мне как-то сложно это представить.


                          1. Fesor
                            16.02.2017 10:49

                            Вот вы бы как реализовали это требование без геттеров и публичных свойств?

                            $orderLine = $product->order($quantity, $bonusCalculator);

                            это если влоб. А уже метод order возьмет свой стэйт, который нужен, и даст его калькулятору бонусов.


                            Словом не зря же закон Деметры придумали.


                            Добавляем в процедуру оформления заказа новое правило с проверкой

                            ключевое слово — процедура. Если мы говорим в терминах старого доброго процедурного программирования, а не объектов и сообщений, то да, нужен доступ к стэйту. Вот только есть проблема. Частенько "приоткрывая" стэйт мы тем самым порождаем простор для дублирования логики.


                            А со скрытым состоянием придется его открывать

                            В том то и дело, что не должно быть такой необходимости. Ну то есть суть не в том что бы делать все свойства приватными по умолчанию а потом "открывать доступ". А в том что бы этого доступа не нужно было давать. Либо объект свой стэйт сам дает своим зависимостям, либо это не нужно.


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

                            ну и т.д.


                            Но чтобы вообще всё скрывать, мне как-то сложно это представить.

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


                          1. Fesor
                            16.02.2017 11:41

                            Вот вы бы как реализовали это требование без геттеров и публичных свойств?

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


                            Потому попробуем порассуждать.


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


                            Когда мы делаем заказ, мы можем либо кинуть доменный ивент что "такой-то заказ был сделан" либо явно дернуть модуль бонусов (смотря какой уровень связанности вас больше устраивает, вдруг у вас микросервисы всякие).


                            Модуль бонусов берет список продуктов из заказа и проверяет что продукты подпадают под спецификацию:


                            if ($product->matches($specification)) {
                                // добавить бонусов
                            }

                            По такому же принципу можно реализовать например какие-то хитрые скидки. Профит:


                            • мы более явно задали способ описания правил начисления бонусов
                            • держим связанность под контролем
                            • не нарушаем инкапсуляции и не закона Деметры
                            • спецификации можно реюзать для поиска по каталогу например.


    1. VolCh
      08.02.2017 10:27

      Состояние сущности может быть не валидно для одной цели, и валидно для другой. Тут мы упираемся в «Контекстуальную валидацию».

      Речь о валидности, точнее консистенции самой сущности, о соблюдении её (и только её) инвариантов. Если для каких-то целей нужно более строгое состояние сущности чем задано её инвариантами, то есть два варианта:
      — наследование (теоретически идеально подходит, на практике многие не рекомендуют применять)
      — дополнительная валидация в сущности (в широком смысле слова, в DDD это может быть, например, доменный сервис) ответственной за достижение целеи или выполнение какого-то этапа её достижения, например, с помощью спецификаций или банальных гетеров/иззеров/хэзеров в целевой сущности и их вызова в ответственном месте.
      Например, когда инварианты сущности клиента разрешают её создание без адреса, а для целей оформления заказа он обязтелен, то пишем код типа
      class Client
      {
          private $id;
          private $address;
      
          public function __construct(string $id, string $address = null)
          {
              $this->id = $id;
              $this->adrress = $address;
          }
      
          public function hasAddress()
          {
              return $this->address !== null;
          }
      }
      
      class CreateOrderService
      {
          public function execute(Client $client)
          {
              if (!$client->hasAddress()) {
                  throw new LogicException("Client hasn't address");
              }
              // Order creation
          }
      }
      
      


      Может проще не использовать генерацию PK на стороне СУБД, а воспользоваться генераторами UUID?

      UX против, UUID сложно передавать по, например, телефону. Хотя всё чаще думаю о создании в таких случаях двух ключей — первичного UUID и обычного INT/BIGINT для UX целей. Или наоборот. Второй вариант подкупает простотой реализации автоинкремента (поддерживается либо средствами СУБД, либо с их помощью ORM), первый — простотой работы с только что созданными сущностями в рамках ORM, но реализация монотонно увеличивающихся локальных идентификаторов не тривиальна, особенно в случае СУБД без сиквенсов (читай — MySQL) даже если разрешено допускать пропуски.


      1. Delphinum
        08.02.2017 12:30

        Например, когда инварианты сущности клиента разрешают её создание без адреса, а для целей оформления заказа он обязтелен, то пишем код типа

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

        Почему все так любят кидаться исключениями при валидации сущности?

        Хотя всё чаще думаю о создании в таких случаях двух ключей — первичного UUID и обычного INT/BIGINT для UX целей. Или наоборот

        Правильное решение. Наоборот не надо.
        реализация монотонно увеличивающихся локальных идентификаторов не тривиальна

        Не понял, где вы в UUID нашли монотонно увеличивающиеся локальные идентификаторы?


        1. Sufir
          08.02.2017 12:44
          +1

          Почему все так любят кидаться исключениями при валидации сущности?

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

          В более сложных ситуациях можно применить спецификацию, о ней тоже есть у Эванса, Фаулера и Вернона.

          В начале статьи указано:
          Для понимания материала понадобится базовое представление о предметно-ориентированном проектировании.

          В конце статьи список материалов по теме, начните оттуда. Если же вы знакомы с DDD, но просто не хотите применять данный подход — то какой смысл писать в данной теме, всё описанное имеет смысл только в рамкам проектирования по модели.


          1. Delphinum
            08.02.2017 12:53

            Потому что вы смешиваете клиентскую валидацию

            Что за «клиентская валидация»? Вы наверно говорите про «предупреждающую валидацию»? Нет, я не о ней, я о «контекстуальной валидации». И почему выполнение бизнес-требований должно ограничиваться «валидацией до первой ошибки»?
            В более сложных ситуациях можно применить спецификацию.

            Нет, спецификация это уже из другой оперы.
            В конце статьи список материалов по теме, начните оттуда

            Если вы догмат теории, тогда рекомендую следующие ссылки для понимая того, о чем я говорю:
            Первоисточник шаблона
            Мнение Фаулера по теме


            1. Sufir
              08.02.2017 13:15
              +1

              Я не «догмат» и даже не догматик, просто вы пишите вещи напрямую не связанные с узким вопросом рассмотренным в статье, все эти вопросы значительно шире.

              А спецификация именно об этом (Э. Эванс «Предметно-ориентированное проектирование (DDD): структуризация сложных программных систем», ИД «Вильямс» — 2011 г., стр. 205) или я не понял о чём вы вообще.

              И почему выполнение бизнес-требований должно ограничиваться «валидацией до первой ошибки»?
              А какая разница какое там количество ошибок? Первая — это уже не выполнение требуемых инвариантов.

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


              1. Delphinum
                08.02.2017 13:24

                вы пишите вещи напрямую не связанные с узким вопросом рассмотренным в статье

                На самом деле связанные, но да, охватывают немного другие темы, разве это плохо?
                А спецификация именно об этом

                Не совсем. Спецификация это только часть решения. Кстати да, тоже очень интересная тема.
                А какая разница какое там количество ошибок?

                Очень большая разница. Полная валидация сущности позволит:
                1. Залогировать всю информацию о причинах отказа в обслуживании
                2. Сообщить клиенту о всех причинах отказа в обслуживании

                Более того, я думаю, что описанный мной построение объекта-сущности никак ей не противоречит

                На самом деле контекстуальная валидация это альтернатива инвариантной валидации.

                Напишите хорошую статью о контекстной валидации, это будет очень интересно

                Писал уже, может скопирну на хабр когда нибудь.


                1. VolCh
                  08.02.2017 14:54

                  1. Залогировать всю информацию о причинах отказа в обслуживании
                  2. Сообщить клиенту о всех причинах отказа в обслуживании

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


                  1. mayorovp
                    08.02.2017 14:55

                    Если операция в отдельном объекте — то да. Но не всегда имеет смысл выносить операцию в отдельный объект.


                    1. VolCh
                      08.02.2017 15:02

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


                      1. mayorovp
                        08.02.2017 15:08

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


                        1. VolCh
                          08.02.2017 16:35

                          Можно и так, если конструкции типа

                          if ($entity->canDoIt()) {
                            try { 
                              $entity->doIt();
                            } catch (Exception $e) {
                              // handle fatal
                            }
                          } else {
                            //.. do smth else
                          }
                          

                          нравятся больше чем
                          try { 
                            $entity->doIt();
                          } catch (EntityCantDoItException $e) {
                            //.. do smth else
                          } catch (Exception $e) {
                            // handle fatal
                          }
                          
                          


                          1. mayorovp
                            08.02.2017 16:39

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


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


                  1. Delphinum
                    09.02.2017 01:13

                    то ничто не мешает

                    Мешает выброс Exception при валидации на первой же ошибке. Зачем этот выброс там нужен, я не понимаю. Исключительная ситуация это та, которой не должно произойти, а если выполняется валидация, то не валидное состояние сущности это не исключительная, а вполне нормальная ситуация, которая как то должна обрабатываться при возврате методом валидации false, и try/catch это не подходящий обработчик для этой ситуации, так как подменяется операция ветвления if/else на try/catch, что делать нежелательно.


                    1. VolCh
                      09.02.2017 07:35

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


                      1. Delphinum
                        09.02.2017 12:14

                        попытка приведения сущности в невалидное состояние, нарушение её инвариантов — исключительная ситуация

                        То есть вы считаете, что сообщить пользователю лучше: «ошибка, заполняйте форму сначала» — чем: «неверно введен адрес электронной почты и телефон»?

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

                        Зачем, когда можно обеспечить защиту на уровне фронтенда, а если там она не сработает (к примеру логика фронтенда отключена пользователем), то на уровне самой сущности?


                        1. VolCh
                          09.02.2017 14:01
                          +1

                          То есть вы считаете, что сообщить пользователю лучше: «ошибка, заполняйте форму сначала» — чем: «неверно введен адрес электронной почты и телефон»?

                          Лучше сообщить пользователю 500 Internal Server Error, если он свою невалидную форму умудрился пропихнуть через фильтры фронтенда, контроллера и сервиса приложения.


                          1. Delphinum
                            09.02.2017 14:06

                            Понятно. Ну я предпочитаю не размывать валидацию безнес-модели на уровне контроллеров и сервиса (если сервис вообще реализуется), а держать ее на уровне домена.


                            1. Sufir
                              09.02.2017 14:31
                              +1

                              На уровне модели предметной области — выполнение ограничений предметной области (domain model). Валидация введенных пользователем данных — это задачи слоя отображения (presentation layer) и прикладного слой (application layer).

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


                              1. Delphinum
                                09.02.2017 14:39

                                Валидация введенных пользователем данных — это задачи слоя отображения

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

                                Другими словами, я вполне допускаю наличие сущностей с невалидным состоянием, но я допускаю использование этой сущности только с валидным состоянием (чувствуете разницу?).

                                Ваша валидация и красивости отображения ошибок к домену ни каким боком не относятся

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


                            1. VolCh
                              09.02.2017 14:38

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

                              Грубо, правило валидации пользовательского ввода: поле 'name' веб-формы регистрации заявки не должно быть пустым, а правило валидации бизнес-модели: создаваемое в ходе процесса регистрации заявки значение $application->$person->$personalData->$name не должно быть пустым.


                              1. Delphinum
                                09.02.2017 14:41

                                А это не размывание валидации бизнес-модели, а отдельные валидации пользовательского ввода и валидация бизнес-модели

                                Что сразу создает дублирование, так как требует валидации как на уровне контроллера, так и на уровне бизнес-модели. Добавление еще одного потока ввода (к примеру из сторонней системы или микросервиса) потребует дополнительного дублирования.

                                Грубо, правило валидации пользовательского ввода: поле 'name' веб-формы регистрации заявки не должно быть пустым, а правило валидации бизнес-модели: создаваемое в ходе процесса регистрации заявки значение $application->$person->$personalData->$name не должно быть пустым

                                А зачем это дублирование?


                                1. oxidmod
                                  09.02.2017 14:45
                                  +2

                                  решается это валидацией команды.
                                  в контроелере из реквеста создается команда и валидируется команда.
                                  из консольного ввода создается таже команда и валидируется теми же правилами.

                                  Если уж запустился команд хеендлер, то значит даные валидны.

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


                                  1. Delphinum
                                    09.02.2017 14:48

                                    решается это валидацией команды

                                    Это так же решается валидацией состояния сущности перед использованием. Получили вы от пользователя данные формы для регистрации нового Клиента, выполнили метод isValidPersist, если ошибок нет, то создаете новую сущность, если ошибки есть, обрабатываете их каким либо образом. На мой взгляд это намного проще и понятнее, нежели ввод самого шаблона «Команда», только для валидации входных данных.


                                    1. oxidmod
                                      09.02.2017 15:03

                                      https://youtu.be/Nsjsiz2A9mg?t=863

                                      Это гораздо проще чем 100500 isValidFor*


                                      1. Delphinum
                                        09.02.2017 15:10
                                        -1

                                        Не заметил простоты.

                                        Это гораздо проще чем 100500 isValidFor*

                                        Вас смущает решение или 100500 методов? Если второе, то можно вынести все валидаторы в один большой «божественный объект», и использовать его )


                                        1. oxidmod
                                          09.02.2017 16:05
                                          +2

                                          Меня смущает, что вы вносите в сущность знание обо всех юз-кейсах её использования.
                                          При появлении нового юзкейса вы добавлете новый метод isValidFor*
                                          Вы дублируете код, что если isValidForOrder еквивалентно isValidForCredit? не будете же вы в при оформлении кредита дергать isValidForOrder?


                                          1. Delphinum
                                            09.02.2017 16:16

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

                                            // Клиенты, для которых можно создать заказ
                                            $clients = $clientRepository->fetchAll(new CanMakeOrder());
                                            


                    1. Sufir
                      09.02.2017 08:41
                      +1

                      Исключительная ситуация это та, которой не должно произойти

                      Именно так, это она и есть.


                      1. Delphinum
                        09.02.2017 12:15

                        То есть пользователь в принципе не должен вводить неверные данные в формы? )

                        С другой стороны, зачем тогда вообще формировать метод isValid, который либо срабатывает, либо выбрасывает исключение, может лучше сразу перехватывать ошибки уровня языка?


                        1. Sufir
                          09.02.2017 14:23

                          А что это за метод и где вы его собираетесь «формировать»?


                          1. Delphinum
                            09.02.2017 14:45
                            -2

                            Так он в вашем примере (название правда другое, но это не важно):

                            class Order {
                                function checkout($client, ...)
                                {
                                    if (!$client->hasManager()) {
                                        throw new ClientHasntManagerException();
                                    }
                                }
                            }
                            

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


                            1. Sufir
                              09.02.2017 15:02

                              Нет, этот метод явным образом в коде описывает бизнес-требования, к валидации он никакого отношения не имеет. Есть требование сформулированное бизнесом «нельзя оформить заказ на клиента без менеджера» (вот просто нельзя никогда, бизнесу не интересно как ты технически это реализуешь, ему важно что бы правило выполнялось) и оно в представленном коде легко прочитывается.

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

                              В предметной области есть Клиент и Заказ, заказ может быть «Оформлен», Клиент может быть «Переименован» и т.д., поведение явно отражает бизнес-логику и единый язык. В этом основной смысл проектирования по модели — предметно-ориентированного проектирования. Ни о каких setSomething() и isValid() бизнес не знает и в проектируемой модели их быть не должно (иногда, конечно, технические ограничения требуют введения служебных методов в классы модели).


                              1. Delphinum
                                09.02.2017 15:18
                                -2

                                Нет, этот метод явным образом в коде описывает бизнес-требования, к валидации он никакого отношения не имеет

                                То есть валидирующий состояние сущности метод не имеет к валидации никакого отношения? )

                                Вы вынесли в сущность Заказ метод, который проверяет стороннюю сущность и называется checkout (проверять, контролировать), но при этом говорите, что в модели не должно быть технических валидаторов ))

                                Ну ок, назовите метод не isValidPersist, а canBePersist чтобы он соответствовал единому языку и отражал бизнес-правило: нельзя регистрировать Клиента без Адреса. Так будет лучше?


                                1. mayorovp
                                  09.02.2017 15:27
                                  +1

                                  ЁПРСТ ИКЛМН! "checkout" переводится не как "проверять, контролировать", а как "выписывать, оформлять"!


                                  В данном случае, речь идет об оформлении заказа, а не о его проверке!


                                  1. Delphinum
                                    09.02.2017 15:30
                                    -1

                                    Значит метод оформления заказа отвечает за контроль состояния объекта, дабы он отвечал бизнес требованию и за, собственно, оформление этого самого заказа? )


                                    1. mayorovp
                                      09.02.2017 15:31

                                      Нет. Метод оформления заказа отвечает за оформление заказа. Он должен либо оформить заказ, либо сообщить о том, что это невозможно.


                                      1. Delphinum
                                        09.02.2017 15:33

                                        Но метод не сообщает о том что это невозможно, метод падает с исключением. Или для вас это одно и то же?


                                        1. mayorovp
                                          09.02.2017 15:35

                                          В данном случае — да, это одно и то же. Потому что попытка оформления невозможного заказа является исключительной ситуацией.


                                          Вот если бы метод не падал с исключением, а возвращал булево значение или более сложную структуру — тогда бы я сказал что у него смешаны две ответственности (оформление заказа и его проверка).


                                          1. Delphinum
                                            09.02.2017 15:40

                                            Он должен либо оформить заказ, либо сообщить о том, что это невозможно

                                            Значит правильнее сказать, что метод либо оформляет заказ, либо падает исключением. Метод не сообщает о том, что это невозможно, так как исключение это не сообщение.

                                            Я же предлагаю вообще не пытаться вызывать метод оформления заказа, который помимо этого оформления должен еще «знать» о бизнес-условиях этого действия и выполнять валидацию всех сущностей, с которыми работает, а вынести эту логику в отдельный валидатор, оформленный в виде метода или класса. Плюсы в том, что:
                                            1. Валидация выделяется из метода, который этой валидацией заниматься не должен
                                            2. Валидация может использоваться повторно и комбинироваться
                                            3. Вы получается подробную информацию о всех причинах отказа при выполнении операции


                                            1. mayorovp
                                              09.02.2017 15:43

                                              1. Delphinum
                                                09.02.2017 15:48

                                                Да, есть такая вещь, но что нам мешает оспаривать ее эффективность и/или приводить альтернативные решения?

                                                Кстати да, еще одна польза контекстной валидации: вы можете использовать ее вместе с защитным программированием, просто выполняя еще одну валидацию внутри метода, от нее зависящего. Если валидация не выполняется, то выбрасывается уже ваше любимое исключение. Выгода тут в том, что вы не дублируете код валидатора, а оформляете его в виде бизнес-требований и используете повторно перед вызовом метода и внутри метода (если вдруг, каким то чудом первая проверка пропустила невалидную сущность, но это уже из области 1С и излишне).


                                                1. mayorovp
                                                  09.02.2017 15:53

                                                  В таком случае я не понимаю о чем вы вообще спорите.


                                                  1. Delphinum
                                                    09.02.2017 15:54

                                                    Эмм… Я удивляюсь выбросу исключений при валидации бизнес моделей и предлагаю обратить внимание на контекстную валидацию.


                                                    1. mayorovp
                                                      09.02.2017 15:58

                                                      Задача метода checkout — не в валидации. Исключения он должен выбрасывать согласно принципу fail fast. Контекстная валидация — опциональна, ее лучше делать, но можно и пропустить если ее логика слишком простая (в примере с checkout — проверяется только 1 свойство).


                                                      1. Delphinum
                                                        09.02.2017 16:04

                                                        Задача метода checkout — не в валидации… Контекстная валидация — опциональна

                                                        Согласно DDD вы как хотите в коде оформить проверку бизнес-требований, если в задачу метода checkout она не входит?


                                                        1. mayorovp
                                                          09.02.2017 16:06

                                                          Чем кроме названия метод hasManager не подходит в качестве метода контекстной валидации? :)


                                                          1. Delphinum
                                                            09.02.2017 16:11

                                                            1. Контекстная валидация, по моему мнению, должна предоставлять вам информацию о причинах невалидности сущности
                                                            2. Контекстная валидация позволила бы нам вообще отказаться от метода hasManager, инкапсулируя его в логике валидатора, а не вынося в публичный API класса
                                                            3. Если бизнес-требования изменятся, то, возможно, проверки одного только hasManager станет недостаточно, и тогда придется искать весь продублированный код по проекту вместо того, чтобы изменить один только валидатор
                                                            4. Валидатор с именем canMakeOrder более информативен и больше соответствует единому языку (на мой взгляд), чем выброс исключений и их обработка через try/catch


    1. maghamed
      09.02.2017 19:34
      +1

      Автор прав, идеально добиться состояние, когда сущность после создания всегда находится в валидном состоянии, т.е. инварианты всегда соблюдаются.
      Есть давняя статья Грега Янга (создателя термина CQRS как паттерна построения приложений) — http://codebetter.com/gregyoung/2009/05/22/always-valid/
      Основная идея там: «it is not that an object must always be valid for everything, there are lots of context based validations… Always valid means that you are not breaking your invariants.»

      Т.е. есть контекстные проверки по которым сущность будет не валидна, но все инварианты должны выполняться.
      Например, у вас вашего объекта Client есть поле email
      По контекстной проверке (проверка на то, что имейл должен быть корпоративным либо принадлежать какому-то специфичному домену) объект может не проходить.
      Но то, то что формат имейла правильный — это invariant который должен выполняться.


      1. Delphinum
        10.02.2017 12:07

        валидном состоянии

        Валидном для чего?


        1. pbatanov
          11.02.2017 18:19

          Мне кажется, вы пришли к конечным автоматам. Посмотрите на компонент workflow в symfony


          1. Delphinum
            11.02.2017 20:28
            -1

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

            Автор идет по другому пути, при котором переход в конкретное состояние невозможен в принципе, и не важно, почему.

            Symphony workflow позволяет определить причину невозможности перехода?


            1. VolCh
              12.02.2017 10:32

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


              1. Delphinum
                12.02.2017 14:46

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


                1. VolCh
                  12.02.2017 15:21

                  Пока контекст один — можно без проблем. Собственно концептуальная валидация будет являться и инвариантом модели. Но чем дальше, чем больше контекстов, тем больше их будет и в сущности, она станет центром всех контекстов. Одних только методов типа isValidFor может стать сотни. И за все контексты отвечать будет сущность — это очень далеко от SRP.


                  1. Delphinum
                    12.02.2017 18:49

                    Если валидатор реализовать в виде класса-спецификации, то можно будет использовать его в контроллере перед вызовом операции.


                    1. VolCh
                      12.02.2017 19:09

                      Не важно как реализовать, можно в контроллере дергать метод hasManager(), можно натравливать спецификацию ReadyToOrder, которая будет его дергать, главное что проверка состояния сущности на валидность для какого-то узкого контекста будет проводиться снаружи сущности, пускай и на основе её состояния.


                      1. Delphinum
                        12.02.2017 20:55

                        Тут важно не местонахождение валидатора, а момент валидации. Инвариант запрещает существование сущности в некорректном состоянии, а контекстная валидация запрещает переход сущности в некорректное состояние. В этом то и разница.


                        1. VolCh
                          13.02.2017 00:35

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


                          1. Delphinum
                            13.02.2017 01:35

                            Как в примере с заказами — создание заказа не изменяет сущность клиента

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


                            1. VolCh
                              13.02.2017 14:15

                              Контекстная валидация — инвариант всей системы, зачастую не относящийся к состоянию одной конкретной сущности, а комбинации состояний разных, типа можно иметь в системе клиента без менеджера и заказ с (задача на доставку — отдельная сущность) доставкой, но нельзя чтобы заказ с доставкой был у клиента без менеджера.

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


                              1. Delphinum
                                13.02.2017 15:44

                                Собственно это я и говорю )


  1. reforms
    07.02.2017 21:32

    Буду благодарен за отзывы и конструктивные замечания.

    Что так: суть статьи и ее посыл понятны + делитесь опытом — за это спасибо.

    Что не так для меня:
    1. ClientBuilder — лишний контракт.
    2. MySqlClientBuilder размывает границы доступа к БД в приложении и усложняет процесс его понимания

    Как бы сделал я:
    1. Сделал объект Client настолько простым, насколько это возможно, в данном случае список полей и get/set методы к ним
    2. Объект доступа к БД, ClientDao с операциями saveClient, loadClients, updateClient и т.д.
    3. Объект проверки сущности Client в ClientValidator — проверка заполнености всех данных, а также их прикладная согласованность.
    4. В слое бизнес логики перед сохранением проверка Client с помощью ClientValidator

    Итого имеем: строго формализованные слои приложения:
    • Слой 'Бизнес Объект' — Client
    • Слой 'Объект доступа к данным' — ClientDao
    • Слой 'Проверки объектов' — ClientValidator
    • Слой 'Бизнес логики' — где происходит манипуляция остальными вышеперечисленными слоями.


    1. VolCh
      08.02.2017 10:43
      +1

      Слой 'Бизнес Объект'

      Это не объект, а так, структура из Си или запись из Паскаля. В PHP вообще массив может быть. DTO в лучшем случае. Собственно геттеры и сеттеры в вашем подходе не нужны, достаточно публичных свойств. Суть бизнес-объекта (сущности) именно в том, что он инкапсулирует относящуюся к нему (и только к нему) бизнес-логику. В общем случае у него не должно быть сеттеров, да и геттеры не на все свойства обычно нужны. И он должен быть максимально сложным (в рамках SRP), а не максимально простым, чтобы максимально использовать преимущества ООП. А у вас ООП вырождается в процедурное программирование с неймспейсами.

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


  1. VolCh
    08.02.2017 10:32
    -1

    По-моему, обычно, для создания сущности с обязательными параметрами, билдер — оверинженеринг. Обязательные параметры в конструкторе, пускай их с десяток (несвязанных друг с другом!) — норма. Билдеры хорошо подходят когда есть необязательные параметры — первым вызовом создаём обязательные, а дальше если нужно в данном кейсе заполняем необязательные, в том числе инкапсулируя создание объектов-значений.


  1. avloss
    08.02.2017 10:53

    Спасибо за статью! всё шикорно! только одна проблемка, как бы пользователт этой системы не начали передавать сам обьект билдера (недосозданный)


    $halfBakedClient = $builder->setId($id)
        ->setName($name)

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


    1. Delphinum
      08.02.2017 12:36

      А что плохого в передаче полу заполненного Строителя?


      1. mayorovp
        08.02.2017 13:11

        Полузаполненный строитель со временем вытесняет использование того объекта, который предполагалось им строить. Особенно когда код отдается фрилансерам в условиях постоянно меняющихся требований.


        1. Delphinum
          09.02.2017 01:17

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

          Такого в принципе быть не может, так как у Строителя одна задача, а у создаваемого им объекта совершенно другая. Другими словами у них разная семантика, чтобы такое проворачивать. Конечно если наговнокодить, то можно все, но делать это не надо и проблема тут не в передаче полузаполненного Строителя.


    1. VolCh
      08.02.2017 14:59
      +1

      Наоборот, это очень крутая возможность передавать строитель в различные функции, чтобы они его дозаполняли по необходимости. Главная функция заполнения может даже не знать о том, что и как заполняют и заполняют ли вызываемые функции и не заполнять ничего сама. А «полусозданные» объекты запрещаются на самом последнем этапе работе билдера, на собственно создании объекта. Если данных недостаточно, то объект не создаётся.


  1. oxidmod
    08.02.2017 11:13

    Есть мнение, что строитель — негодный паттерн


    1. maghamed
      09.02.2017 19:40

      Паттерн как паттерн, для случая, который описывает автор — фабрика тоже подойдет. Тут не это главное в статье автора.


  1. boom
    09.02.2017 10:37

    Спасибо за статью, но
    во-первых:

    $client = $builder->setId($id)
        ->setName($name)
        ->setGeneralManagerId($generalManager)
        ->setCorporateForm($corporateForm)
        ->setAddress($address)
        ->buildClient();
    

    зачем? почему? есть много способов использовать это не правильно. Почему не
    $client = $builder->buildClient($id,
        $name,
        $generalManager,
        $corporateForm,
        $address);
    

    ну или именованный конструктор.

    во-вторых: Скорее всего Менеджер — это отдельная сущность, Адрес хоть и Value object, но хранится в отдельной табличке. Не дорого ли, ради DDD? клиентов, к примеру, вы можете восстанавливать из базы в 20 разных местах бизнесс-логики, но Менеджер используется в одном, Адресс используется в другом, и в итоге в 18 местах вы восстанавливаете из хранилища «консистентного» клиента с 2мя дополнительными запросами (ну или джойнами). Это мне никак не понять в DDD, как-будто это несущественные затраты, но нам, например, при отдаче REST запроса, оч важно сэкономить и 10ms (есть требование, чтобы ответы были в пределах 100ms) и вот эти лишние данные в клиенте в 80% случаях не нужны, но по DDD сущность нужно восстанавливать из хранилица тоже консистентной, даже если вы не пользуетесь всей сущностью.


    1. boom
      09.02.2017 10:56

      Наверно в моем случае правильнее использовать CQRS :)
      Но всеравно есть агрегаты, которые тяжело восстанавливаются (Ну например, зачем восстанавливать аггрегат заказа с 1000 строк в заказе? везде и всегда)


      1. VolCh
        09.02.2017 11:44

        CQRS и DDD в общем случае дополняют друг друга. Например, одни сервисы приложения реализуют команды, а другие запросы, не смешивая их, команды возвращают максимум true/false, а запросы ничего не меняют (кроме разве что логов технического аудита).

        Есть ленивая загрузка графов объектов из базы. В контексте PHP Doctrine это умеет более-менее. А Value object в отдельной табличке обычно костыль, чтобы применить ту схему данных, которая нравится, а не которое предлагается той же Doctrine.


        1. maghamed
          09.02.2017 19:50
          +1

          идея в том, что для операций чтения, которых в большинстве приложений сильно больше чем записи вы можете иметь отдельную упрощенную структуру данных, т.е. Query операции вам возвращают просто массив данных key=>value или массив DTO. А в Command у вас полноценные модели, с процеркой всех валидацией и тяжелыми связями для обеспечения консистентности

          В CQRS подходе DDD у вас живет только в коммандах, квери должны быть очень упрощенными и быстрыми.
          Как правило с отдельным индексом и без тяжелого ORMа


          1. VolCh
            12.02.2017 10:39

            Как вариант упрощенной структуры данных может быть вытягивание в те же классы сущностей DDD без всех их связей, без отслеживания изменений в объектах после вытягивания и т. п. Это позволяет, с одной стороны, хранить иметь только одну модель и только один маппинг на базу, не заморачиваясь с синхронизацией при каждом чихе, а, с другой, позволяет выполнять запросы заметно быстрее чем команды.


            1. maghamed
              12.02.2017 11:13

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


              1. VolCh
                12.02.2017 15:23

                Применимо. И масштабирую отдельно — чтение с рид-онли реплик БД.


    1. maghamed
      09.02.2017 19:45

      то что вы написали, это реализация фабрики. Билдер это как раз про цепочки сеттеров.

      А по поводу вопросов дороговизны. Если ваши сущности иммутабельны, т.е. не изменяемы, то вы вполне можете их шарить по всему приложению, так как никто не сможет их поломать изменив глобальный стейт. И у вас есть некий Object Manager — глобальный контейнер в который кладутся уже созданные сущности и который отвечает за создание новых, которые требуются параметрами конструкторной dependency injection. То это сильно сократит стоимость.

      Но если у вас очень нагруженное приложение и требование по перформансу очень высоки, то DDD может быть действительно не для вас.


  1. maghamed
    09.02.2017 19:23

    Ну, сам процесс создание объектов-сущностей не большая проблема. И даже большое кол-во зависимостей может быть не проблемой (хотя большое кол-во зависимостей это «smell» в коде, который намекает, что ваш объект берет на себя много обязанностей и вероятно нарушает принцип единой ответственности, что всегда плохо в ООП) если у вас есть Object Manager (Entity Manager/DI Container) который проинстанциирует все внешние зависимости сущности.

    Но конкретно в вашем примере нельзя Client называть Enitity. У вас это классический Data Transfer Object, т.к. вы показали что он хранит только данные и аксессоры к этим данным. И никаких методов бизнесс логики


    1. Sufir
      09.02.2017 23:38

      Нет, это конечно же не DTO и я начинаю с того, что бы как раз сделать шаг от анемичной модели к rich и превратить DTO в Entity. Но да, мы уже обсудили это в другом месте, моя ошибка. Для упрощения примера и акцента на теме создания объектов не стал упоминать о поведении, но из-за этого пример выглядит неубедительно. Хотя бы вкратце это оговорить нужно было.


      1. maghamed
        12.02.2017 11:18

        DTO который содержит пачку геннеров и сеттеров — это не всегда плохо, точней если он используется для межпроцессной коммуникации (например в web api), то это даже хорошо.
        Поэтому может просто имеет смысл выделить отдельную сущность — доменную модель. А DTO оставить как DTO. Сериализируемый объект, который всегда можно передать по сети.


        1. VolCh
          12.02.2017 15:33

          Лучше выделить DTO (если он вообще нужен, особенно в PHP, где вокруг динамических ассоциативных массивов чуть ли не бОльшая половина экосистемы крутится) из сущности прозрачно замапленной на БД, чем делать ещё один слой сохранения данных поверх доктрины.

          В теории вроде разницы нет, но на практике DTO между доменной моделью и слоем хранения данных сильно замедляет разработку, не давая особых плюсов, пока возможностей используемой ORM не ограничивают (или ограничивают но не сильно) доменную модель. А когда ограничивают, то чаще всего это уже просачиваются через ORM ограничения РСУБД, а не самой ORM. Ситуации когда четко видишь как разработанная модель красиво ложится на SQL БД, но при этом ORM не даёт так положить — редки.


        1. Sufir
          13.02.2017 10:04

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


          1. maghamed
            13.02.2017 12:29

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


            1. VolCh
              13.02.2017 14:24

              Вообще говоря не задача сущности изолировать свои данные от внешнего (по отношению к домену) мира. Это задача уровня сервисов приложения. Они обращаются к сущностям и сервисам модели как часть домена и, если нужно, изолируют домен от остального приложения, в частности возвращая вместо сущностей DTO, часто заточенный под конкретный запрос приложения.


  1. alxsad
    13.02.2017 21:35
    +1

    Пару замечаний.

    Для идентификации сущностей лучше использовать именно UUID, это лучше и с точки зрения безопасности, ведь часто авто инкременты фигурируют в URL, откуда третьи лица могут, например, сделав дифф двух значений, узнать сколько у вас было транзакций в день, что может являться бизнес тайной.

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

    Есть хороший туториал от создателей doctrine.


    1. VolCh
      14.02.2017 10:23
      +1

      Для идентификации сущностей лучше использовать именно UUID

      Нет однозначного ответа, что лучше. UUID лучше с точки зрения простоты разработки и безопасности, но хуже с точки зрения юзабилити (ссылку по телефону затруднительно передать будет, например или отсортировать объекты в порядке создания без создания дополнительных полей) и, обычно, потребляемых ресурсов, особенно если бинарное представление недоступно и используется какое-то из символьных.


      1. Delphinum
        14.02.2017 10:37
        -1

        Не рекомендуется использовать ключи идентификации для целей, отличных от идентификационных, таких как — сортировка, юзабилити и т.д. У каждого элемента должна быть минимальная ответственность.


      1. alxsad
        14.02.2017 12:56
        +1

        Для сортировки есть UUID, сгенерированные на основе timestamp, а для ссылок лучше использовать отдельное поле с «красивым» значением, авто инкременты лучше не показывать пользователям с точки зрения безопасности и конфиденциальности бизнеса.


        1. VolCh
          14.02.2017 20:38

          Если с красивым (номер документа, например), то с точки зрения безопасности и конфиденциальности бизнеса разницы нет между автоинкрементным первичным ключом и генерируемым последовательно номером.


          1. alxsad
            15.02.2017 11:31

            Пример «красивого» идентификатора без номера youtube.com/watch?v=ysM-Z_lLcEU


            1. VolCh
              15.02.2017 12:12

              Чем он красивый? Как его продиктовать?


              1. alxsad
                15.02.2017 13:01

                Так же как и цифровой, да, сложнее, но безопаснее. Вообще кейс странный, не припомню когда мне приходилось надиктовывать ссылки. Можно скинуть, например, в мессенджер.


                1. VolCh
                  15.02.2017 16:07

                  Вполне нормальный кейс когда речь идёт о корпоративном софте, в частности об удобстве топ-менеджеров, которые не хотя добавлять в свои мессенджеры рядовых работников.


                  1. alxsad
                    15.02.2017 16:12
                    +1

                    Не понимаю почему диктовать ссылку удобнее чем переслать по той же корпоративной почте.


                    1. VolCh
                      15.02.2017 16:51

                      Лишние движения для топ-менеджера.


                      1. alxsad
                        15.02.2017 17:08

                        Да ладно, пару кликов.


                        1. VolCh
                          16.02.2017 07:50

                          Еще кнопочки нажимать. Да и вообще когда гендир звонит в саппорт, можно и премии лишиться, если сказать «а вы мне письмо напишите»"


                          1. alxsad
                            16.02.2017 11:51

                            Такие компании проходят мимо :)


                1. Delphinum
                  15.02.2017 16:36
                  +1

                  Можно скинуть, например, в мессенджер

                  Вы это скажите тёте-секретарю 50 лет, которая забивает документы в СЭД и периодически звонит в УФССП для уточнения чего либо по определенному делу. Нет, пользоваться скайпом/мылом/аськой и т.д. она не умеет, с трудом научили вбивать данные в поля и нажимать «Сохранить».


                  1. alxsad
                    15.02.2017 16:59

                    О чем вообще речь? Я выше писал, что для публичных ссылок лучше не использовать инкременты, вы сейчас говорите о внутренних процессах тети-секретаря. В первом случае ссылки могут содержать UUID, во втором — «красивые» номера для внутреннего пользования тетями-секретарями. Причем можно иметь сразу два идентификатора для каждой сущности.


                    1. VolCh
                      15.02.2017 17:01

                      авто инкременты лучше не показывать пользователям с точки зрения безопасности и конфиденциальности бизнеса.

                      Где слово «публичные»?


                      1. alxsad
                        15.02.2017 17:04
                        +1

                        Ясно, разошлись с самого начала, все что я писал выше о безопасности, относится к случаям, когда идентификаторы могут видеть люди, не обремененные всякими NDA.


                    1. Delphinum
                      15.02.2017 23:43

                      Причем можно иметь сразу два идентификатора для каждой сущности

                      Значит я вас не правильно понял, прошу простить. Полностью согласен, для человека лучше применять отдельный формат нумерации, а не идентификатор сущности.


      1. alxsad
        14.02.2017 13:05
        +1

        Еще одно преимущество использования UUID — масштабирование, когда у вас «распределенная» модель базы данных, можно быть уверенным что идентификаторы не будут иметь дубликатов на различных серверах.


        1. VolCh
          14.02.2017 20:43
          +1

          Никто не мешает иметь два ключа )) Вопрос какой из них первичный.


    1. Fesor
      14.02.2017 23:04
      +1

      Не согласен с тобой относительно билдеров.


      У меня тут очень простая позиция. У методов не должно быть более 3-х аргументов. То есть если у сущности есть 5 обязательных параметров, вместо того что бы делать конструктор с 5-ю аргументами проще сделать билдер. Это будет и читабельнее, и удобнее в использовании.


      В целом для сущностей я обычно по умолчанию билдеры юзаю. Для VO уже намного реже. Иногда вместо билдера еще фабрики можно делать + dto какое-то… это не столь важно. Важно чтобы все данные передавались одним пакетом и связанность по данным была ниже.


      Что до туториалов: вот еще на эту тему


      1. alxsad
        15.02.2017 11:41

        Все зависит от конкретного случая, 5 аргументов нормальная ситуация если они скаляры. Для сложных сущностей, где куча аргументов VO, я использую фабрики, имхо лучше подходят нежели билдеры.


        1. Fesor
          16.02.2017 09:21
          +1

          5 аргументов нормальная ситуация если они скаляры.

          А в чем разница можешь сказать? Ну вот у тебя есть класс. По твоей логике если я беру класс у которого 5 аргументов в конструкторе, все хорошо пока они скаляры. А если я эти скаляры оберну в свой тип (Email, Password, etc) то уже не ок что-ли? Почему раньше тогда было ок?


          1. alxsad
            16.02.2017 12:26

            Кейс выдуманный для примера

            new User(
            new Name(new FirstName('John'), new LastName('Doe')),
            new Age(18),
            new Username('johndoe'),
            new Email('john.doe@gmail.com'),
            new Password('qwerty')
            )

            vs

            new User('John Doe', 18, 'johndoe22', 'johh.doe@gmail.com', 'qwerty')

            В первом случае для создания сущности придется писать много кода, особенно в тестах, поэтому можно написать builder/factrory.


            1. Fesor
              16.02.2017 12:44

              по этой причине я использую билдеры/фабрики)


              1. alxsad
                16.02.2017 12:47

                Ну так я это и имел ввиду, все зависит от кейса, я считаю что для второго кейса (описал выше) писать билдер/фабрику излишне.


        1. mayorovp
          16.02.2017 09:27

          Вообще-то наоборот. 5 аргументов — это нормально если они не скаляры. Потому что скаляры не могут сами себя документировать, особенно если они константы.


          1. Fesor
            16.02.2017 09:50

            Проблема со связанностью по данным, а скаляры это или нет не имеет никакой разницы. Потому есть всякие правила:


            • идеальный метод имеет 0 аргументов (тут только message coupling, самый низкий вид связности к которому нужно стремиться)
            • 1 аргумент норм (но уже начинается data coupling)
            • 2 аргумента ну так, сойдет (data coupling выше)
            • 3 аргумента — край. (еще выше).

            data coupling это конечно не content coupling но все же уровень этого вида связности нужно держать под контролем.


            1. mayorovp
              16.02.2017 10:00

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


              Если есть вызов вида foo(5, 3.0, false, "bar", null) — то проблема не в data coupling, а в том, что чтобы понять что эта строчка делает надо несколько раз "прыгнуть" между этой строчкой и определением метода foo. А если у параметров еще и типы одинаковые — то все вовсе печально.


              При вызове же метода с передачей ему пяти объектов такой проблемы не возникает. И тут уже можно начинать рассуждать о том что data coupling — это плохо :)


              1. Fesor
                16.02.2017 10:11

                то проблема не в data coupling, а в том, что чтобы понять что эта строчка делает надо несколько раз "прыгнуть" между этой строчкой и определением метода foo.

                вменяемые IDE решают эту проблему запросто. А вот проблему data coupling не решают.


                В целом же даже если брать ваш вариант — либо у вас код будет выглядеть как-то так:


                foo(new Foo(5), new Bar(3.0), State::invalid(), new Baz('bar'));

                либо вы не решили проблему и нам всеравно нужны подсказки и хинтинги от IDE.


                1. mayorovp
                  16.02.2017 10:39

                  В IDE код пишется. А вот читаться он может в форме дифов в гите. Или через веб-интерфейс...


                  1. Fesor
                    16.02.2017 10:50

                    В любом случае разницы не будет. Что так не удобно что эдак. С билдерами же будет явно и читабельно.


              1. VolCh
                16.02.2017 10:24
                +1

                в том, что чтобы понять что эта строчка делает надо несколько раз «прыгнуть» между этой строчкой и определением метода foo

                Даже если IDE не подскажет, то всегда можно сделать что-то типа:
                $orderCountLimit = 5;
                $discontPercent = 3.0;
                $delivery = false;
                $customerComment = "bar";
                $customerEmail = null;
                foo($orderCountLimit, $discontPercent, $delivery, $customerComment, $customerEmail);
                

                Ничем не хуже:
                $orderCountLimit = new OrderCountLimit(5);
                $discontPercent = new Discont(3.0); // предполагаем, что конструктор устанавливает свойство percent
                $delivery = new Delivery(); // false значение по умолчанию
                $customerComment = new Comment("bar"); 
                $customerEmail = new NullEmail();
                foo($orderCountLimit, $discontPercent, $delivery, $customerComment, $customerEmail);
                

                А то и лучше, ведь от скаляров никуда не делись.


          1. alxsad
            16.02.2017 12:46

            Оборачивать скаляры в объекты ради документации кода? Так себе идея, тем более есть php7 и доктайпы в IDE.


      1. VolCh
        15.02.2017 12:14

        А внутри билдер вызывает конструктор с пятью аргументами или конструктор с тремя и два сеттера потом?


        1. alxsad
          15.02.2017 12:58

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


          1. Fesor
            16.02.2017 09:18
            +1

            сеттеры это плохо.


            сущность должна быть валидной всегда.

            именно так.


            1. alxsad
              16.02.2017 12:33

              Серега, никто не спорит что сеттеры хорошо, это был синтетический кейс.


        1. Fesor
          16.02.2017 09:15

          public function build(): User
          {
              return new User($this);
          }

          public function __construct(UserBuilder $builder)
          {
              $this->email = $builder->email();
              $this->credentials = new Credentials($this, $builder->email(), $builder->password());
              $this->profile = $builder->profile();
              $this->registeredAt = new \DateTime;
          }

          $user = User::builder()
              ->setPassword($password, $passwordEncoder)
              ->setEmail($email)
              ->setProfile(Profile::builder()
                  ->setName($name)
                  ->setBirthday($birthDay)
                  ->setPicture($picture) 
                  ->build()
              )
              ->build();

          как-то так обычно.


          1. alxsad
            16.02.2017 12:39

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

            $user = new User(
            $email,
            $password,
            $passwordEncoder,
            new Profile($name, $birthDay, $picture)
            );


            1. Fesor
              16.02.2017 12:47

              1. больше простора для дальнейших действий
              2. меньше связанность по данным (не так больно добавлять новый обязательный параметр)
              3. проще хэндлить валидацию
              4. в тестах можно реюзать билдеры:

              $customer = UserFixtures::customerBuilder()->build();
              $referral = UserFixtures::customerBuilder()->setIntroducer($customer)->build();

              выходит весьма и весьма удобно.


              1. alxsad
                16.02.2017 12:55

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


                1. Fesor
                  16.02.2017 15:59

                  Не понимаю насчет связанности

                  Есть такой вид связности — связность по данным.


                  ты отвязываешь сущность от доменных объектов и привязываешься к хелперу-билдеру

                  сущность и есть доменный объект. Я просто делаю что-то типа объекта-сообщения который содержит все необходимые сущности данные (вместо полотнища аргументов которые не читабельный и неудобны при расширении логики).


                  И это не "хелпер-билдер", это часть предметной области. Те данные которые необходимы для регистрации пользователя. DTO если хочешь.


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


                  1. alxsad
                    16.02.2017 17:18

                    Под доменными объектами я имел ввиду всякие VO в аргументах сущностей, хотя если DTO часть домена, то и билдеры норм, вопрос вкуса, мне нравятся простые фабрики.


                    1. Fesor
                      16.02.2017 19:59

                      Простые фабрики тоже ничего.


  1. ghost404
    16.02.2017 10:27
    +1

    Мой комментарий получился немного великоват и я оформил его в виде отдельной статьи:
    https://habrahabr.ru/post/321892/


    1. Sufir
      17.02.2017 12:25
      +1

      Интересно и по теме, хотя и не совсем непосредственно ответ. Команды (DTO) и их обработчики я отношу к прикладному слою, а не к домену, и скармливаю их командной шине. Статья не о том в целом, вы скорее развиваете вопрос. Любопытно, спасибо за ответ и за DDD!


      1. ghost404
        17.02.2017 20:07

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