Это прекрасно.
Моделирование по философии Domain Driven Design. Понятие доменной модели. Исследование предметной области и выделение ограниченных контекстов.
Анализ бизнеса аукциона мы проведём отдельно в серии о техзадании.
Чтобы не пропускать новые эпизоды подпишитесь на наш канал @deworkerpro в Telegram

Это прекрасно.
Стало еще лучше, появились картинки :) . Мне не хватало рисунков в разработке рабочего места и деплоя. Прожал на будущую кнопку "лайк".
Спасибо, все лаконично и этим круто, и отдельное спасибо за kleki. Также недавно открыл для себя draw.io, удобно набрасывать схемки клиенту с любого устройства.
Ждем с нетерпением картинок с кодом :)
Спасибо!!! А почтовый сервер будет подниматься в этом мастер-классе или будут заглушки?
Будут заглушки для девелопмента.
Для борьбы с антиспамом удобнее использовать сторонние сервисы, чем поднимать савой почтовик.
Да, было бы круто почтовый сервер (напр. image: tvial/docker-mailserver:latest) и UI с ROUNDCUBEMAIL_DB_TYPE=pgsql....
Спасибо, Дмитрий.
Спасибо за подробно изложенный материал!
Доброй ночи, Дмитрий! С праздником 9 мая!)
Проектируя по DDD ограниченными контекстами решил выделить все справочники в отдельный контекст: Справочники (Dictionaries). И появилось несколько вопросов:
В других сервисах я ссылаюсь на этот справочник только по 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
Это дело вкуса.
Очень полезно, спасибо!
Спасибо.