DDD и ограниченные контексты

Моделирование по философии Domain Driven Design. Понятие доменной модели. Исследование предметной области и выделение ограниченных контекстов.

Скрытый контент
Комментарии (29)
Arunas

Спасибо.

Ответить
Denis

Это прекрасно.

Ответить
Ruslan

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

Ответить
Руслан

Спасибо, все лаконично и этим круто, и отдельное спасибо за kleki. Также недавно открыл для себя draw.io, удобно набрасывать схемки клиенту с любого устройства.

Ждем с нетерпением картинок с кодом :)

Ответить
BMWist

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

Ответить
Дмитрий Елисеев

Будут заглушки для девелопмента.

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

Ответить
Arunas

Да, было бы круто почтовый сервер (напр. image: tvial/docker-mailserver:latest) и UI с ROUNDCUBEMAIL_DB_TYPE=pgsql....

Ответить
Александр

Спасибо, Дмитрий.

Ответить
Роман

Спасибо за подробно изложенный материал!

Ответить
Максим

Доброй ночи, Дмитрий! С праздником 9 мая!)

Проектируя по DDD ограниченными контекстами решил выделить все справочники в отдельный контекст: Справочники (Dictionaries). И появилось несколько вопросов:

  1. Уместно ли такое название для справочников? Какое название используете вы?
  2. В справочник у меня ушли: категории мероприятий, категории организаций, страны регионы города, пол человека и так далее. В других сервисах я ссылаюсь на этот справочник только по ID (Category ID). Правильно ли это? Вы говорили, что сервис должен быть полностью независим. А тут вроде как зависимость. если не сложно - поясните)
Ответить
Дмитрий Елисеев

В других сервисах я ссылаюсь на этот справочник только по ID (Category ID). Правильно ли это?

Правильно. Должно быть только указание ID. Это не связь с самим объектом.

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

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

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

Ответить
Максим

Благодарю! А если категории мероприятий используется в двух местах?

Ответить
Дмитрий Елисеев

Тогда уже можно вынести.

Ответить
Максим

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

В своих уроках вы часто используете в качестве ID - UUID. Но везде использовать UUID не даёт понимания при анализе базы данных. Поэтому некоторые ID формируют более смысловые. Например первый символ может быть Первой буквы названия сервиса. Или используя сущность + другие значения. В итоге из тематики медицины есть такой пример: PatientTHX1138

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

И можно ли использовать SLUG в качестве ID? Только вот проблема. Слуг иногда меняется. Например у пользователя или организации.

Ответить
Дмитрий Елисеев

Но везде использовать UUID не даёт понимания при анализе базы данных. Поэтому некоторые ID формируют более смысловые.

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

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

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

В итоге из тематики медицины есть такой пример: PatientTHX1138. И можно ли использовать SLUG в качестве ID?

Это всё определяет вопрос, какой идентификатор использовать: суррогатный (UUID, Auto Increment) или естественный (slug, email, patient). В случае суррогатного его формат не важен и значение точно никогда не изменится.

А с пользовательскими уже сложнее. Например, для записи PatientTHX1138 таблицу patients можно создать с составным первичным ключом (id=1138, type=TNX).

Так что либо UUID или Auto Increment, либо что-то своё, либо хранить одновременно UUID для связей + что-то другое вроде slug для отображения посетителю.

Ответить
Максим

И ещё забыл уточнить. Важный момент. У меня много категорий для мероприятий, для организаций, для дисциплин. У меня есть два пути: 1. В категории добавить type и указывать (type = events, discipline, organization). В этом случае большой плюс, что не надо будет городить много сущностей, верстки и т.п. 2. Каждая категория отдельно, но так как большинство будут использоваться в 2-х и более местах придется поместить в справочник. Тогда будут названия сущностей вроде EventCategory OrganizationCategory...

Какой подход 1 или 2 лучше?

Ответить
Дмитрий Елисеев

Если разделять, то больше гибкости, так как можно программировать их со своими нюансами независимо. В этом случае проблема лишней вёрстки и кода частично решается разработкой общих виджетов и сервисов, работающих с interface Category или TreeCategory и подобными обобщениями. Если не разделять, то сэкономим на коде, но везде будем таскать type. Так что вопрос выбора сложный.

Ответить
Дмитрий

А подскажите, пожалуйста, как поступить в ситуации, если у сущности в различных состояниях могут быть (не)заполнены какие-то поля? Например, возьмём модель такси, там есть сущность заказа, у которой при статусе new поле driver пустое, но через какое-то время водитель берёт заказ и заказ переходит в статус booked. Т.е. получается статус заказа и водитель являются неким инвариантом, при каком-то статусе водителя нет, а при каком-то есть. Т.е. если подходить к вопросу в лоб, то не получается соблюсти данный инвариант:

class Order {
   private Status $status;
   private ?Driver $driver = null;
  ....
}

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

Ответить
Дмитрий Елисеев

Для каждой операции делаем метод, переводящий всё сразу в новое состояние:

class Order
{
    ...

    public function bookBy(Driver\Id $driver, DateTimeImmutable $date): void
    {
        if ($this->status->isBooked()) {
            throw new DomainException('Order is already booked.');
        }
        $this->status = Status::booked();
        $this->bookedAt = $date;
        $this->driver = $driver;
    }
}

и в нём производим все заполнения с проверками.

Но вместо одного большого класса Order можно разбить процессы на несколько более мелких классов. Тогда взятие заказа можно производить отдельным классом Booking:

$booking = new Booking($orderId, $driverId, $date);

Тогда внутри Booking эти поля будут сразу обязательными.

Ответить
Дмитрий

Да, отдельным классом наверно это хорошее решение. Т.к. тогда это решит проблему, что $order->getDriver() когда-то есть, а когда-то нет. Спасибо.

Ответить
Дмитрий Елисеев

С одним классом с методами:

public function isBooked(): bool
public function getDriver(): ?Driver

тоже проблемы нет.

Ответить
Дмитрий

Да, но тогда как решить проблему?

if ($order->isBooked()) {
    // type: ?Driver, а хотелось бы просто Driver
    $driver = $order->getDriver(); 
}
Ответить
Дмитрий Елисеев

Если это проблема, то решить как:

public function getDriver(): Driver
{
     return $this->driver ?? throw new BadMethodCallException('Driver is not set.');
}
Ответить
Андрей

Добрый день, Дмитрий!

Хотел бы узнать ваше мнение по правильному применению практик DDD.

У моем проекте модель предметной области DDD разделена на 3 модуля, каждый из которых размещен в собственном каталоге (условно Model1, Model2, Model3). Должны ли корневые сущности-агрегаты моделей всех 3-х модулей реализовывать один общий интерфейс RootAggregateInterface или допустимо, чтобы каждый из 3-х модулей реализовывал свой собственный интерфейс RootAggregateInterface со своими индивидуальными методами, необходимыми только конкретному модулю?

Ответить
Дмитрий Елисеев

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

Ответить
Андрей

Добрый день, Дмитрий!

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

Правильно ли я понимаю, что, например, в Вашем подходе к разработке на Slim классы сущностей относятся к уровню модели, классы репозиториев и Fluser - к инфраструктурному уровню, классы внутри UseCase (Command, Handler) - к операционному уровню, а классы Action - к презентационному уровню.

Одним словом, хотелось бы увидеть что-то типа этого.

Спасибо за вашу работу.

Ответить
Дмитрий Елисеев

Очень не хватает обзорного видео по общим принципам DDD

Принципы DomainDD - это единый язык с бизнесом и ограниченные контексты. Аналогично с этим TestDD лишь призывает писать тесты до кода. А BehaviorDD призывает делать тесты, описывающие поведение системы на человекопонятном языке, а не в техническом виде. Как именно писать сам код тестов эти практики явно не говорят.

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

Хотелось бы услышать о луковичной, гексагональной и иных архитектурах приложения

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

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

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

Горизонтальное разделение на уровни нам менее важно, чем вертикальное разделение на модули.

Ответить
Андрей

Дмитрий, спасибо за Ваш развернутый ответ.

Заинтересовала Ваша схема в комментариях с разбиением по слоям:


Application
    Command
    Query

Domain
    Entity
        class Id
        interface UserRepository
    Service
        interface PasswordHasher
    interface Flusher

Infrastructure
    Entity
        class IdType
        class DoctrineUserRepository implements UserRepository
    Service
        class CryptPasswordHasher implements PasswordHasher
    class DoctrineFlusher implements Flusher

Возник вопрос: в каком пространстве имен правильно будет располагать интерфейсы доменного уровня: interface UserRepository, interface PasswordHasher и interface Flusher? Все интерфейсы расположить в одном пространстве имен \App ? Или существует какой-то более грамотный подход?

Ответить
Дмитрий Елисеев

Для простого приложения можно именовать всё по папкам от корневого App:

App\Application
App\Domain
App\Infrastructure

Но вместо App можно папке src присвоить нэймспейс по имени приложения:

Blog\Application
Blog\Domain
Blog\Infrastructure

Если приложение модульное, то можно делать модули внутри App:

App\Blog\Domain
App\Comment\Domain

либо обходиться без App:

Blog\Domain
Comment\Domain

Это дело вкуса.

Ответить
Зарегистрируйтесь или войдите чтобы оставить комментарий

Или войти через:

Google
GitHub
Yandex
MailRu