Domain-Driven Design: тактическое проектирование. Часть 2



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

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

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

    Традиционно, начиная с книги Э.Эванса, первым рассматривается шаблон DDD – сущность.

    Сущность (Entity)


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

    Есть несколько стратегий создания идентификаторов:

    Ввод пользователем уникального значения

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

    Идентификатор генерируется приложением

    Существуют быстрые и высоконадежные генераторы, которые можно использовать в приложениях для автоматического создания уникального идентификатора. Например, в среде Java существует класс java.util.UUID, который позволяет генерировать так называемый универсально уникальный идентификатор (universally unique identifier) четырьмя различными способами (time-based, DCE security, name-based, randomly generated UUIDs).

    Примером такого UUID является: 046b6c7f-0b8a-43b9-b35d-6489e6daee91. То есть 36 байтовая строка.

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

    Для примера можно рассмотреть такой идентификатор: APM-P-08-14-2016-046B6C7F.

    Здесь: АPM – отдельный контекст управления проектированием; P – проект; 08-14-2016 – дата создания; 046B6C7F- – первый сегмент UUID. Когда такие идентификаторы попадаются в различных ограниченных контекстах, разработчики сразу видят, откуда они появились.

    Идентификатор генерируется механизмом постоянного хранения

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

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

    Идентификаторы, которые присваиваются другими ограниченными контекстами

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

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

    Для сущности, обычно, кроме основного идентификатора предметной области, используется суррогатный идентификатор. Первый подчиняется требованиям предметной области, а второй предназначен уже для самого инструмента ORM (как Hibernate). Для создания суррогатного ключа обычно создается атрибут сущности типа long или int, в базе данных создается уникальный идентификатор, и он используется как первичный ключ. Потом включается отображение этого ключа в атрибут с помощью инструментов ORM. Такой суррогатный идентификатор обычно скрывают от внешнего мира, так как он не является частью модели предметной области.

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

    Для начала необходимо выделить сущности в предметной области. Совершенно очевидно, что существует сущность BankingAccount, и для ее идентификации напрашивается использование номера счета accountNumber. Тем не менее этот номер уникальный только в отдельном банке и может повторятся в других банках. (Можно использовать номер IBAN, но этот номер в основном используется только в банках Европейского союза.) То есть помимо номера счета еще можно использовать сегмент UUID. Тогда наш идентификатор будет состоять из PFM-A-424214343245-046b6c7f, где:

    • PFM – имя контекста
    • A – Account
    • 424214343245 – номер счета accountnumber
    • 046b6c7f – часть из UUID


    Идентификатор можно задать как объект-значение. Необходимо детально рассмотреть этот очень важный шаблон DDD.

    Объект-Значение (Value Object)


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

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

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

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

    Для объекта-значения очень важно определять операцию проверки равенства. Чтобы два объекта-значения были равны, необходимо, чтобы все типы и значения атрибутов были равны.

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

    Рассмотрим классический пример объекта значения денежная сумма (во многих примерах в интернете встречается этот класс):

    public class Money implements Serializable {
    private BigDecimal amount;
    private String currency;
    public Money (BigDecimal anAmount, String aCurrency) {
        this.setAmount(anAmount);
        this.setCurrency(aCurrency);
    }
    …
    }

    Методы установщики здесь делаются скрытыми, создание объекта значения – атомарная операция. Примером конкретного значения есть {50 000 долларов}. По отдельности эти атрибуты либо описывают что-то другое, либо ничего конкретного не означают. Особенно это относится к числу 50000 и в некоторой степени к долларам. То есть эти атрибуты образует концептуально целое значение, которое описывает денежную сумму. Такая целостность концепции в предметной области играет очень большую роль. Важно понимать, что именуются типы значений и сами значения в соответствии с единым языком в своем ограниченном контексте.

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

    Служба Предметной Области (Domain Service)


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

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

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

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


    Событие (Domain Event)


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

    • «Когда… »
    • «Если это случится… »
    • «Сообщите мне, если… » или «уведомьте меня, если… »
    • «В случае… »

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

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

    Для примера можно рассмотреть событие FundsDeposited:


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

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

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

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

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

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

    Существует много компонентов для передачи сообщений, которые относятся к классу промежуточного программного обеспечения (например, RabbitMQ, NServiceBus). Можно также сделать обмен сообщениями с помощью ресурсов REST, где автономные системы обращаются к издательской системе, требуя уведомления о событиях, которые еще не были обработаны.

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

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

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

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

    Давайте рассмотрим следующий шаблон DDD.

    Модуль (Module)


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

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

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

    com.bankingsystems

    Следующий сегмент имени модуля идентифицирует ограниченный контекст. Желательно, чтобы имя этого сегмента основывалось на имени ограниченного контекста:

    com.bankingsystes.pfm

    Далее следует квалификатор, который идентифицирует модуль именно предметной области:

    com.bankingsystems.pfm.domain

    Все модули модели можно поместить именно в этом разделе domain. Вот так:

    com.bankingsystems.pfm.domain.account
      <<Entity>>BankingAccount
      <<ValueObject>>AccountId

    В известной всем многоуровневой архитектуре именование было бы таким:

    сom.bankingsystems.resources
    сom.bankingsystems.resources.viewуровень пользовательского интерфейса (хранение представлений)

    сom.bankingsystems.application
    сom.bankingsystems.application.accountприкладной уровень (подмодуль прикладных сервисов)

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

    Агрегат (Aggregate)


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

    У каждого агрегата есть корень (Aggregate Root) и граница, внутри которой всегда должны быть удовлетворены инварианты.

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

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

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

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

    Каждый агрегат может хранить ссылку как корни других агрегатов. При этом это не помещает этот агрегат в границы согласованности первого агрегата. Ссылка не порождает целостный агрегат.

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

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

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

    В качестве примера агрегата можно привести кредитный отчет:


    Каждый кредитный отчет должен содержать данные об идентификационных данных заемщика. Для этого мы сохраняем внешнюю связь по идентификатору сustomerId. Сustomer – совершенно другой агрегат, который содержит информацию о заемщике (имя, адрес, контакты). Инвариантом, допустим, будет правило пересчета кредитного рейтинга – в зависимости от данных кредитной истории.

    Совершенно неважно, как именно он пересчитывается, главное, – чтобы выполнялась транзакционная согласованность внутри агрегата. Например, после добавления или замены записи кредитной истории, показатель кредитного рейтинга должен сразу же пересчитаться. Это должна быть атомарная операция. Если используется база данных, должна быть отдельная транзакция. Сразу после внесения данных в объекты внутри агрегата, должны выполняться все инварианты.

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

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

    Фабрика (Factory)


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

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

    Лучше выполнять создание сложных агрегатов или других объектов отдельно. Для этого используются фабрики. Фабрики – элементы программы, обязанности которого создавать другие объекты.

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

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

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

    Хранилища (Repository)


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

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

    Есть два типа проектов хранилищ:

    1. Ориентированные на имитацию коллекций;
    2. Ориентированные на механизм постоянного хранения.

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

    Представить такое хранилище можно как HashMap<ObjectId,Object>. В этой коллекции исключается повторная вставка одного и того же элемента. При этом, получая и изменяя объект, изменения фиксируются сразу.

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

    1. Неявное копирование при чтении (implicity copy-on-read) (механизм постоянного хранения копирует каждый раз объект хранения при чтении из базы данных и сравнивает закрытую копию с клиентской при фиксации транзакции);
    2. Неявное копирование при записи (механизм управляет загруженными объектами с помощью прокси-объекта).

    Такой механизм, как Hibernate, позволяет создавать хранилище, ориентированное на имитацию коллекции.

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

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


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

    Здесь используется первый тип хранилища, ориентированный на имитацию коллекции. В нем необязательно реализовать метод save() или put(). Только методы коллекции.

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

    Вывод


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

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

    Надеемся, что статья помогла вам продвинуться в понимании шаблонов DDD! Если есть вопросы, пишите в комментариях. С радостью ответим. И спасибо за внимание.

    Статью подготовили: greebn9k (Сергей Грибняк), wa1one (Владимир Ковальчук), silmarilion (Андрей Хахарев).
    Share post

    Similar posts

    Comments 26

      +1
      В рамках одной транзакции в ограниченном контексте должно происходить изменение только одного агрегата.

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

        0
        Я тоже так подумал, но в книге «Реализация методов предметно-ориентированного проектирования» на этом акцентировалось внимание несколько раз, поэтому добавил это.
        0
        А можно подробнее раскрыть тему агрегатов?
        Правильно ли я понимаю, что область применения этого шаблона, в рамках концепции DDD — только всякого рода отчёты? Как они соотносятся с сущностями?
          +1
          агрегат — это граф сущностей, используемых в одном контексте. Например Order (Заказ) и OrderLines (Строки Заказа). OrderLines в отрыве от заказа неуместны, поэтому они используются только в составе агрегата Order, который состоит из корня агрегата/сущности Order и связанных с ним сущностей OrderLines.
            +1

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

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

              До какой степени выделять?
              Допустим товар может иметь такие свойства: цвет и объем памяти.
              Но выделять все свойства в отдельную сущность — вряд ли правильно при большом числе свойств. :)

              На счет генерирования ИД можно поговорить и в отдельной статье. :)
                +2

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


                Сущность — это то, что имеет идентичность, которая не выражается через его данные (сложно сказано).


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


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

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

                    Больше интересует даже другой вопрос:
                    Хранить ли справочник «память» и «цвет» в одной таблице или в разных. :)
                      0
                      Это не вопрос моделирования :) Вы можете моделировать цвет отдельной сущностью с кучей методов типа «повеселее», а хранить простой строкой в таблице основной сущности.
                        0
                        Ну так весь вопрос как такие сущности-полусущности хранить в одной таблице свойств товаров…
                        С учетом того, что что-то будет отдельной сущностью (зачем?), а что-то нет. :)
                          0
                          В одной таблице с сущностью легко можно хранить ValueObject, который может занимать несколько столбцов типа memory_amount и memory_unit, которые будут маппиться на entity.memory.amount и entity.memory.unit, а может храниться в одном столбце типа memory в сериализованном виде типа {«amount»: 512, «unit»: «MB»}. С сущностями можно тоже так поизвращаться, но обычно смысла нет.
                            0
                            Это все общий текст.
                            Покажите пример реализации :)

                            Хранить сериализировано — тот еще адок. :)
                            В mysql json только недавно добавили. То есть на старших версиях фильтрация и сортировка работать не будут.
                            Ну и индексов на это не навесить :)
                              0
                              doctrine embeddables — пример реализации первого варианта
                                0
                                  0
                                  В Doctrine есть оба. Фильтрация, сортировка, связность реализуются на уровне приложения.
                                    0

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


                                    Вообще DDD в php лучше реализовывать через Doctrine.

                        0
                        Модуль в понятиях DDD может ли содержать в себе сервисы из инфраструктурного или слоя приложения?
                        Условно говоря — модуль «оформления заказа» имеет в себе, помимо доменных моделей, ещё и инфраструктурные шлюзы оплаты или что-либо в таком духе.
                          0
                          модуль имеет внутри себя все, что нужно для его работы, то есть да, он должен иметь все слои. Нюанс: оплата может стать неплохим пластом кода, поэтому, возможно, следует вынести в отдельный модуль, а в первом оставить лишь интерфейс сервиса оплаты.
                            0
                            Модули внутри модели являются именованными контейнерами для некоторой группы объектов предметной области

                            For a large and complex application, the model tends to grow bigger and bigger. The model reaches a point where it is hard to talk about as a whole, and understanding the relationships and interactions between different parts becomes difficult. For that reason, it is necessary to organize the model into modules.

                            Здесь, в статье, и в DDD Quickly везде написано что «Модуль» включается в себя разбитую на куски «Модель» когда она слишком разрастается.
                            Это не доменная модель? Что это за модель тогда? «Модель» = «модель всей предметной области» = «всё моё приложение»? Если так, то да — всё приложение целиком включает в себя любые необходимые слои. Но если имеется ввиду разбиение именно доменной модели, то тут не могут быть никакие другие слои, кроме непосредственно доменного.
                              0
                              да, имеется в виду доменная модель, как ядро модуля, вокруг которого он строится. Но это не значит, что модуль состоит только из слоя домена.
                                0
                                Но какой же может быть cohesion внутри модуля между слоями?

                                DDD Quickly:
                                While cohesion starts at the class and method level, it can be applied at module level. It is recommended to group highly related classes into modules to provide maximum cohesion possible.


                                Но тут опять же ничего не сказано про другие слои. Я могу интерпретировать эту фразу как: связывай доменные модели, но не делай связи между слоями внутри модуля.
                                  0
                                  обычный cohesion.
                                  Надо отметить, что в DDD quickly, указан повод для выделения домена в модуль, но не говорится, что в модуле должен быть только домен, поскольку модуль — это не ddd-понятие, а общий термин программирования.
                                  вот например реализация приложения из классической книги Эванса https://github.com/codeliner/php-ddd-cargo-sample/tree/master/CargoBackend/src
                                  как видим в модуле присутствуют все 4 слоя.
                            0
                            Я стараюсь выносить шлюзы и прочую инфраструктуру вовне модулей домена. Доменные модели и сервисы используют интерфейсы/абстрактные классы, а нужные реализации подтягиваются в рантайме через DIC/SL/etc. Условно — модуль «оформления заказа» содержит абстрактный класс «платежная система» с которым модуль и работает, а экземпляр конкретного класса создаётся на уровне приложения перед тем как обратиться к модулю.

                          Only users with full accounts can post comments. Log in, please.