Pull to refresh

Comments 17

В коде нет нумерации, но статья написана так, как будто она есть. Это конечно не проблема, понять о чем речь, но немного напрягает
К сожалению подсветка кода Хабра не поддерживает номера строк, а в оригинальной статье они есть. Оставил текст как в оригинале.

Что-то меня напрягает параметр DbContext в AddReview.


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


На крайний случай, можно было бы и lazy-loading заюзать.

Да, меня тоже напрягает. Да и автора напрягает. Вообще если в read-стеке всегда делать select в dto, то lazy-load не так уж и страшен
Не соглашусь с некоторыми моментам в статье. DDD — это не про приватные сеттеры и публичные методы с проверками.

Пример в статье очень легко привести в невалидное состояние — например, у Title может быть ограничение в БД, например, в 100 символов, Description — в 500.
Или по бизнес-правилам Title — это строка без переносов и спецсимволов, а в Description может быть html. ImageUrl тут легко может быть не валидным url, а чем угодно, и Price — отрицательным.
Используя String в качестве параметров — элементарно нарушить любые правила и привести модель в невалидное состояние, что для DDD — катастрофа.

Для того, чтобы этого избежать, модель должна работать только с Entity и ValueObject. Title, Description, ImageUrl и Price должны быть отдельными ValueObject, внутри которых есть собственные валидаторы, вызываемые в конструкторе и бросающие исключение, если что то не так. Неплохое решение для валидатора — публичные статические методы, пригодные для валидации значений извне, например полей dto.
А уже все Entity, в нашем случае Book, состоят исключительно из других ValueObject и связанных Entity, которые передаются через конструктор. Таким образом мы в принципе не сможем создать неправильную Entity, и стают ненужными большинство валидаторов в конструкторах и методах Entity. ValueObject'ы же вполне могут быть переиспользованы в любых других местах.
Так же совсем не обязательно сеттеры для всех свойств Entity делать приватными. Если бизнес-правила допускают, что свойство может просто изменяться без каких либо других ограничений, то какой смысл делать сеттер приватным при использовании ValueObject?

Тем более, что EF Core с версии 2.1 позволяет задавать собственные «Value conversions» для таких объектов. Но тут есть некоторые ограничения, главное из которых — если такой ValueObject попадает в expression tree, например — .Where(), то в этом случае из БД загрузяться все обьекты и только потом отфильтруются. Но это можно легко обойти. NHibernate в этом плане гораздо лучше подходит для DDD.

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

Ну и про паттерны Repository и UnitOfWork. Конечно, EF Core это все реализует из коробки.
Но по DDD, Persistence — это отдельный слой на Инфраструктурном уровне, который скрывает в себе все логику хранения данных. Остальные слои ничего про это не должны знать. Если, условно, Persistense убрать из проекта, то остальной код этого даже не должен заметить. Но если мы напрямую передаем контекст БД в Presentation layer или домен — мы тут же жестко связываем эти слои с Persistense и EF Core и раскрываем детали имплементации. А жесткая связь между разными слоями или нюансами реализации в DDD — это провал. Мало того, передавая контекст в остальные слои, мы позволяем там очень много вольностей. По этому в домене должны быть интерфейсы репозиториев, а в Persistense — их реализация ровно в том объеме, который нужен остальным слоям.

DDD вносит очень большой оверхед в разработку, но, как по мне — оно того точно стоит.
Про ValueType с вами согласен на все 100%.

Тем более, что EF Core с версии 2.1 позволяет задавать собственные «Value conversions» для таких объектов. Но тут есть некоторые ограничения, главное из которых — если такой ValueObject попадает в expression tree, например — .Where(), то в этом случае из БД загрузяться все обьекты и только потом отфильтруются. Но это можно легко обойти. NHibernate в этом плане гораздо лучше подходит для DDD.

Не совсем верно. Если например, объявить тип Email, который будет проверять, что строка гарантированно вида #@.#, то в LINQ при сравнении с другой строкой запрос построится корректно, а вот с типом Email EF не сможет. Задача конечно решается через визитор на входе в QueryProvider, но это уже не «из коробки».
Так же совсем не обязательно сеттеры для всех свойств Entity делать приватными. Если бизнес-правила допускают, что свойство может просто изменяться без каких либо других ограничений, то какой смысл делать сеттер приватным при использовании ValueObject?


Потому что в реальном мире у приложения будет какой-нибудь личный кабинет, админка или еще какая-нибудь консоль управления, которая позволит устанавливать значение любых свойств вместе или по отдельности. Таким образом открыть для сеттинга придется почти все поля. А это может привести к тому, что другие разработчики (фанатеющие как раз от анемичной модели) будут игнорировать ваши бизнес-методы и дублировать логику путем насетивания свойств сущности.
Если данные можно менять по отдельности / вместе в любых комбинациях и это не на что не влияет, оставляйте все публичным. Но если это действительно можно делать, то в вашем приложении просто нет никакой бизнес-логики и DDD не нужен.
Я обычно делаю метод edit(), который принимает все редактируемые свойства в качестве аргументов.
Большинство неудобств этого подхода можно выявить при попытке сделать UI для редактирования. Если посмотреть репозиторий, на вот это ChangePubDateService: IChangePubDateService -> public Book UpdateBook(ChangePubDateDto dto) То вот вообще не хочется делать отдельный сервис (+ интерфейс, + DTO) для изменения Title, отдельно для изменения Description, отдельно если надо их изменить одновременно.
Куча boilerplate, ради академичности.
DTO вам в любом случае нужен, если мапинг не 1 к 1, не уйдете. А репозиторий и сервис вам зачем, если уже есть метод UpdateBook?

В примере, кстати Book.UpdatePubDate(date), а не UpdateBook(ChangePubDateDto dto).
Это про BizRunner'ы уже. То что выходит за рамки статьи. По поводу этого кода с вами согласен: много boilerplate из ничего.
А если для инициализации агрегата потребуется over 20 параметров. Делать конструктор с кучей параметров?
И что делать если логика должна предполагать как полную так и частичную инициализацию агрегата?
А если для инициализации агрегата потребуется over 20 параметров. Делать конструктор с кучей параметров?

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

И что делать если логика должна предполагать как полную так и частичную инициализацию агрегата?

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

Я правильно понимаю, сейчас агрегат выглядит так: создаем экземпляр книги, закидываем туда комментарии к этой книге (если у нас 1000 комментариев, то всю тысячу закидываем). Если у комментария есть зарегистрированный автор, то к каждому комментарию цепляется еще объект автора комментария. Если нам нужно вывести топ самых комментируемых книг, то мы получаем несколько таких объектов, каждый из которых имеют этакую «гирю» в виде комментариев внутри себя. Или это как-то по другому работает?
Нет, неправильно. Если вам нужен агрегатор который одновременно работает с 1000 комментами, авторами и списками его книг, а также описанием (я так понимаю вы про подняли всю базу в память), то значит вы смешали контексты предметной области, а значит сама архитектура неверно построена. И сами сущности, инкапсуляция и архитектура слоёв и микро сервисов. DDD это не про «прочитал статью, узнал новую фишку а ля проверяй параметры в конструкторе» и начал использовать. Это целостная концепция, где одно подкрепляет другое.

Более того, есть Lazy Loading который поднимет вам ваши 1000 комментов только если по ним нужно пройтись (если уж так получилось).
ПРичем всё это нужно именно для изменения модели предметной области. Для чтения можно использовать CQRS подход.

А вообще это сферический конь в вакууме. Нужен конкретный пример и на нём я смогут отмести 100500 «а если» и сформировать правильную модель. Иногда даже такую, которую даже сам не ожидал бы увидеть в начале. Просто нужно перестать думать данными и таблицами БД, а начать думать процессами (предметной области, бизнес-сценариями).
Sign up to leave a comment.

Articles