Комментарии 18
var priceInfos = DbContext .CompanyData .ByInnAndKpp("инн", "кпп") .ToList();
Оно же может вызвать NRE из-за того, что ByInnAndKpp может вернуть null, нехорошо.
Непонятно только только, почему при использовании DDD нельзя описывать бизнес логику в сервисах
Ведь DDD — это лишь методология взаимодействия разработчика с заказчиком, а не предписания к архитектуре проекта.:)
Еще интересно было бы узнать как выглядит схема базы данных и как она развивается по ходу проекта
У вас для каждой сущности своя таблица или вы храните все данные в виде набора событий?
нельзя описывать бизнес логику в сервисах
можно, когда она не относится к сущности непосредственно. Это больше вопрос о том, как декомпозировать систему. Выносить всю логику в сущности это такая же крайность как и выносить всю логику в сервисы. Адекватная декомпозиция будет представлять собой различные комбинации.
а не предписания к архитектуре проекта.:)
Идея в том, что бы ваши архитектурные решений руководствовались именно общением с бизнесом и пониманием предметной области. Есть отличный доклад на эту тему: Udi Dahan — If (domain logic) then CQRS, or Saga?. Там он затрагивает вопрос о том, как не заданные бизнесу вопросы могут случайно повлиять на архитектурные решения.
У вас для каждой сущности своя таблица или вы храните все данные в виде набора событий?
event sourcing это совершенно отдельная интересная тема. У меня например — и так и так. Есть 2-3 сущности которые хранятся как стрим событий, и все остальные — просто как таблички в базе (потому что там от сложность от event sourcing не перекрывает профит)
Непонятно только только, почему при использовании DDD нельзя описывать бизнес логику в сервисах
Почему же нельзя? Сервисам посвящен целый параграф. Другой вопрос, что есть смысл хранить не всю логику. Обратите внимание на метод
Decline
. Он четко показывает: чтобы отклонить заявку необходимо добавить непустой комментарий с пояснением, почему компания не может быть аккредитована. Представьте, у нас в домене три бизнес-сценария, где мы должны отклонять аккредитацию. Где гарантия, что все три метода будет реализовывать один разработчик? Даже если так, он может забыть правило про обязательный комментарий. В варианте с отдельным методом это невозможно.Кроме того в реальном приложении при отклонении необходимо было хранить историю заявок, в том числе с указанием менеджера, работающего с юр.лицом. При переходе из одного статуса в другое необходимо было менять 2-3 разных поля по определенным правилам. Объектная модель с поведением здорово помогает с пониманием «что здесь происходит и почему».
Более подробно этот вопрос раскрывает Дино Эспозито. Ближе к концу доклада он приводит следующий пример. Допустим, в поддержку приходит баг. Пользователь объясняет, что он совершил некоторое действие, но ему не были начислены бонусные балы (или типа того). Если в коде программист видит «репозитории» и изменение каких-то свойств ему потребуется приложить дополнительные усилия, чтобы интерпретировать слова пользователя и сопоставить с кодом. А если он видит в коде нечто вроде:
if(user.IsVip)
{
user.AddBonus(100);
}
Он может сразу уточнить статус пользователя. Может быть, про бонусные баллы он услышал от друга, а про VIP-статус позабыл. Может быть у нас баг и пользователю не был присвоен VIP-статус, хотя должен был. В любом случае, диалог получится куда более предметным.
Всегда интересно посмотреть как это делают другие разработчики.
Мы решили полностью следовать советам книги Patterns, Principles, and Practices of Domain-Driven Design. Тут подробно описано, с примерами, что зачем и почему, куда что класть.
Все то что Эванс говорил, но разжевано.
У меня так же есть несколько вопросов по примерам:
Обоснуйте пожалуйста, почему метод Accept принадлежит сущности Company. Ведь аккредитация выдается Ведомством. А то получается что компания сама себя куда хочешь может аккредетировать… Ну или как минимум в этот метод должен передаваться Policy ведомства или экземпляр объекта Аккредитация, с данными кто выдал и почему.
Почему метод DangerouslyChangeInnAndKpp вообще содержит в себе эту приставку? Если предметной области это нормальное явление — это бессмысленно. Может стоило тогда ввести другую сущность?
Я не знаком с .Net вообще, но вы упоминаете «луковую» и «чистую» архитектуры, а потом в домене используете public class Specs. Разве это не кусок вашего фреймворка только что просочился в домен? Т.о. вы нарушаете направление связей.
Обоснуйте пожалуйста, почему метод Accept принадлежит сущности Company. Ведь аккредитация выдается Ведомством. А то получается что компания сама себя куда хочешь может аккредетировать… Ну или как минимум в этот метод должен передаваться Policy ведомства или экземпляр объекта Аккредитация, с данными кто выдал и почему.
Именно в данном приложении ведомство было одно, а аккредитация проходила автоматическом режиме при закрытии квартала при соблюдении определенных условий. Сущность в проекте называется по другому и там довольно увесистый агрегат. Возможно более естественно смотрелись бы названия методов
BecomeAccepted
и BecomeDeclined
, чтобы субъект и объект действия не путались.Почему метод DangerouslyChangeInnAndKpp вообще содержит в себе эту приставку? Если предметной области это нормальное явление — это бессмысленно. Может стоило тогда ввести другую сущность?
Смена ИНН и КПП — это перерегистрация. Т.е. в реальном мире нельзя просто так взять и сменить. Однако, предполагалось, что в подаваемых сведениях могут быть ошибки и к системе были предъявлены требования дать возможность эти ошибки исправлять.
Название
DangerouslyChangeInnAndKpp
навеяно React'ом. Видимо, зря я удалил аннотации, в которых эта логика объяснялась.Я не знаком с .Net вообще, но вы упоминаете «луковую» и «чистую» архитектуры, а потом в домене используете public class Specs. Разве это не кусок вашего фреймворка только что просочился в домен? Т.о. вы нарушаете направление связей.
В .NET на уровне платформы встроены «деревья выражений» (
Expression Trees
). Например:Expression<Func<Company, bool>> acceptedSpec =
x => x.State = CompanyState.Accepted;
Expression<Func<Company, bool>>
— довольно многословная конструкция. Spec<T>
— более читаемая и сразу специализирована для сигнатуры T -> bool
. Плюс добавлена перегрузка операторов &&
и ||
. Спецификация по определению представляет правила бизнес-логики, т.е. является частью домена.Необязательно создавать спецификации внутри класса сущности, можно положить их рядом. Мне просто удобно использовать такой синтаксис:
Company.Specs.Accepted
. Все бизнес-правила фильтрации агрегата Company
под рукой и intellisense поможет их найти. Можно использовать и другие правила, например отдельный статический класс. Тогда будет так: CompanySpecs.Accepted
.Product.BasePrice
и есть CartCalculator
. Внутри калькулятора — еще другие зависимости, которые расчитывают цену в зависимости от лицензии, роялти, накопительной скидки и других бизнес-правил.cartCalculator.Calculate(cart)
, кэп!:) Калькулятор считает ценую корзины, потому что скидки могут применять как позиции заказа, так и заказу в целом. Бывает «3 по цене 2», «купи на 3000 рублей и получи в подарок шнурки для галошь» и просто накопительные скидки для постоянных клиентов.В общем и в целом, считаю, что через конструктор сущности зависимости типа сервисов или стратегий можно внедрять, только используемые непосредственно конструктором, а не остальными методами. Основная причина — проблемы с персистентностью: от ORM или подобного слоя нужно ожидать, что конструктор, тем более с параметрами вызываться ею при восстановлении объектов не будет, она как-то сама заполнит свойства, которые были на момент сохранения. Да, ORM может быть достаточно умной, чтобы восстановить сервис или стратегию, если знает как их получить из, например, DI-контейнера. Но мне сложно представить какие непредвиденные последствия будут у этого, если она будет только думать, что знает, на самом деле ошибаясь.
Если уж хочется через Product::price() получать рассчитанную по стратегии цену, то получайте её как Product::price(PricingStrategy pricingStrategy), в худшем случае через последовательность Product::setPricingStrategy(PricingStrategy pricingStrategy); Product::price() с броском исключения или применением дефолтной стратегии.
Как человеку плохо знакомому с C#, мне кажется, что задача построения системы вокруг домена решена недостаточно тщательно и это мягко говоря. Ощущение, что система построена вокруг фреймворка. Код типа
[Display(Name = "ИНН")]
[Required]
[DisplayFormat(ConvertEmptyStringToNull = true)]
[Inn]
public string Inn { get; protected set; }
прямо кричит об этом. Если я правильно его понимаю (аннотации, используемые для генерирования кода и подобных вещей, не влияющие непосредственно на прямых клиентов объекта, создающих и дергающих его методы), то подобные вещи выношу куда-то в слой инфраструктуры, часто в текстовые (yaml, xml) его конфиги. Да, часто добавление одного поля, сквозного для всей системы от UI до БД, приводит к необходимости по паре строчек добавлять чуть ли не в два десятка файлов, зато при изменении в инфраструктуре "зацепляется" часто только один.
Вообще, начиная со "Структура проекта" статья получилась очень языко- (и, видимо, фреймворко-) специфичной. Возможно, это просто специфика C#, где один фреймворк занимает подавляющее господство, и переезд на другой фреймворк без смены языка вообще немыслим, но вот задачу переноса бизнес-логики на другой стек решать, кажется, будет очень сложно, по крайней мере человеку не владеющему полностью используемым. Просто изучением синтаксиса языка, похоже, не обойтись, без изучения всей экосистемы.
Думаю, статья (или код) сильно бы выиграла с явным указанием мест, где вы считаете, что принципы DDD нарушаются, в идеале с объяснением причин почему, компромисс между чем и чем, что считаете техническим долгом, вернуть который нет свободных ресурсов, а что только выглядит таким, а на самом оптимальное решение для планируемого срока жизни и частоты изменений системы на данном стеке.
Domain Driven Design на практике