Богатые доменные модели - как именно вписывается поведение?

84

В дебатах моделей предметной области Rich против Anemic Интернет полон философских советов, но не хватает авторитетных примеров. Цель этого вопроса - найти четкие рекомендации и конкретные примеры правильных моделей проектирования на основе доменов. (В идеале в C #.)

Для реального примера эта реализация DDD кажется неправильной:

Приведенные ниже модели доменов WorkItem представляют собой не что иное, как пакеты свойств, используемые Entity Framework для базы данных с первым кодом. По Фаулеру, это анемично .

Уровень WorkItemService, по-видимому, является распространенным заблуждением доменных служб; он содержит всю поведенческую / бизнес-логику для WorkItem. По Емельянову и другим, это процедурно . (стр. 6)

Так что, если ниже не так, как я могу сделать это правильно?
Поведение, т.е. AddStatusUpdate или Checkout , должно принадлежать в классе WorkItem правильно?
Какие зависимости должна иметь модель WorkItem?

введите описание изображения здесь

public class WorkItemService : IWorkItemService {
    private IUnitOfWorkFactory _unitOfWorkFactory;

    //using Unity for dependency injection
    public WorkItemService(IUnitOfWorkFactory unitOfWorkFactory) {
        _unitOfWorkFactory = unitOfWorkFactory;
    }

    public void AddStatusUpdate(int workItemId, int statusId) {

        using (var unitOfWork = _unitOfWorkFactory.GetUnitOfWork<IWorkItemUnitOfWork>()) {
            var workItemRepo = unitOfWork.WorkItemRepository;
            var workItemStatusRepo = unitOfWork.WorkItemStatusRepository;

            var workItem = workItemRepo.Read(wi => wi.Id == workItemId).FirstOrDefault();
            if (workItem == null)
                throw new ArgumentException(string.Format(@"The provided WorkItem Id '{0}' is not recognized", workItemId), "workItemId");

            var status = workItemStatusRepo.Read(s => s.Id == statusId).FirstOrDefault();
            if (status == null)
                throw new ArgumentException(string.Format(@"The provided Status Id '{0}' is not recognized", statusId), "statusId");

            workItem.StatusHistory.Add(status);

            workItemRepo.Update(workItem);
            unitOfWork.Save();
        }
    }
}

(Этот пример был упрощен, чтобы быть более читабельным. Код определенно все еще неуклюжий, потому что это ошибочная попытка, но поведение домена было следующим: обновить статус, добавив новый статус в историю архива. В конечном счете, я согласен с другими ответами, это может быть обработан CRUD.)

Обновить

@AlexeyZimarev дал лучший ответ - идеальное видео на эту тему в C # от Джимми Богарда, но, очевидно, оно было перемещено в комментарий ниже, потому что оно не давало достаточно информации, кроме ссылки. У меня есть черновик моих заметок с кратким изложением видео в моем ответе ниже. Пожалуйста, не стесняйтесь комментировать ответ с любыми исправлениями. Видео длится час, но его стоит посмотреть.

Обновление - 2 года спустя

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

введите описание изображения здесь

Я по-прежнему приветствую любые ответы на этот (очень активный) пост, в которых приведен любой передовой код для правильной доменной модели.

RJB
источник
6
Все философские теории падают прямо на землю, когда вы говорите им "I don't want to duplicate all my entities into DTOs simply because I don't need it and it violates DRY, and I also don't want my client application to take a dependency on EntityFramework.dll". «Сущности» в жаргоне Entity Framework - это не то же самое, что «сущности», как и в «Модель предметной области»
Федерико Берасатеги,
Я в порядке с дублированием моих доменных сущностей в DTO, используя автоматический инструмент, такой как Automapper, если это то, что нужно. Я просто не уверен, как это должно выглядеть в конце дня.
RJB
16
Я бы порекомендовал вам посмотреть сессию Джимми Богарда NDC 2012 "Создание моделей злых доменов" на Vimeo . Он объясняет, какой должна быть богатая область и как их реализовать в реальной жизни, имея поведение в ваших сущностях. Примеры очень практичны и все на C #.
Алексей Зимарев
Спасибо, я на полпути к видео, и пока это идеально. Я знал, что если это неправильно, то где-то должен быть «правильный» ответ…
RJB
2
Я тоже требую любви к Java: /
uylmz

Ответы:

59

Самый полезный ответ дал Алексей Зимарев, получивший не менее 7 голосов, прежде чем модератор переместил его в комментарий под моим первоначальным вопросом ....

Его ответ:

Я бы порекомендовал вам посмотреть сессию Джимми Богарда NDC 2012 "Создание моделей злых доменов" на Vimeo. Он объясняет, какой должна быть богатая область и как их реализовать в реальной жизни, имея поведение в ваших сущностях. Примеры очень практичны и все на C #.

http://vimeo.com/43598193

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

  • «Для большинства приложений ... мы не знаем, что они будут сложными, когда мы начнем. Они просто становятся такими».
    • Сложность растет естественным образом по мере добавления кода и требований. Приложения могут запускаться очень просто, как CRUD, но поведение / правила могут быть встроены.
    • «Приятно то, что нам не нужно начинать с комплекса. Мы можем начать с модели анемичной области, это просто пакеты свойств, и только с помощью стандартных методов рефакторинга мы можем перейти к настоящей модели домена».
  • Доменные модели = бизнес-объекты. Поведение домена = бизнес-правила.
  • Поведение часто скрыто в приложении - оно может быть в PageLoad, Button1_Click или часто в вспомогательных классах, таких как «FooManager» или «FooService».
  • Бизнес-правила, которые отделены от объектов домена, требуют, чтобы мы помнили эти правила.
    • В моем личном примере выше одно бизнес-правило - WorkItem.StatusHistory.Add (). Мы не просто меняем статус, мы архивируем его для аудита.
  • Поведение домена «устраняет ошибки в приложении намного проще, чем просто написание множества тестов». Тесты требуют, чтобы вы знали, чтобы написать эти тесты. Поведение домена предлагает вам правильные пути для тестирования .
  • Доменные службы являются «вспомогательными классами для координации действий между различными объектами модели домена».
    • Доменные службы! = Поведение домена. Сущности имеют поведение, доменные службы являются просто посредниками между сущностями.
  • Доменные объекты не должны обладать необходимой инфраструктурой (т.е. IOfferCalculatorService). Инфраструктурный сервис должен быть передан модели домена, которая его использует.
  • Модели доменов должны предлагать вам рассказать, на что они способны, и они должны делать только эти вещи.
  • Свойства моделей предметной области должны быть защищены частными установщиками, так что только модель может устанавливать свои собственные свойства посредством своего поведения . В противном случае это «неразборчиво».
  • Объекты анемичной доменной модели, которые являются просто пакетами свойств для ORM, являются лишь «тонким шпоном - строго типизированной версией над базой данных».
    • «Как легко получить строку базы данных в объекте, это то, что мы получили».
    • «Большинство постоянных объектных моделей - только это. Что отличает анемичную модель предметной области от приложения, которое на самом деле не имеет поведения, так это наличие у объекта бизнес-правил, но эти правила не встречаются в модели предметной области. '
  • «Для многих приложений нет реальной необходимости создавать какой-либо реальный логический уровень бизнес-приложений, это просто то, что может взаимодействовать с базой данных и, возможно, какой-то простой способ представления данных, которые там находятся».
    • Другими словами, если все, что вы делаете, это CRUD без специальных бизнес-объектов или правил поведения, вам не нужен DDD.

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

RJB
источник
Отличное видео, особенно чтобы увидеть, как рефакторинг работает в инструменте. Многое о правильной инкапсуляции объектов домена (чтобы убедиться, что они согласованы). Он отлично работает, рассказывая бизнес-правила о предложениях, участниках и т. Д. Он упоминает слово инвариант пару раз (это моделирование предметной области). Я хотел бы, чтобы .net-код лучше передавал формальное бизнес-правило, поскольку они меняются, и вам необходимо их поддерживать.
Фурманатор
6

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

Например, если требовалось, чтобы конкретный рабочий элемент мог иметь только определенные статусы или чтобы он мог иметь только N статусов, то это логика домена и должна быть частью WorkItemили StatusHistoryкак метод.

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

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

И наконец, кое-что, что я хотел бы упомянуть, заключается в том, что настоящую модель предметной области с реальным дизайном ООП было бы действительно трудно сохранить при помощи Entity Framework. Хотя ORM были спроектированы с отображением истинной структуры ООП на реляционные, все еще существует много проблем, и реляционная модель часто просачивается в модель ООП. Даже с nHibernate, который я считаю гораздо более мощным, чем EF, это может быть проблемой.

Euphoric
источник
Хорошие моменты. К чему бы тогда принадлежал метод AddStatusUpdate, в Data или другом проекте в Infrastrcture? Каков пример любого поведения, которое теоретически может принадлежать WorkItem? Любой псевдо-код или макет будет принята с благодарностью. Мой пример был упрощен, чтобы быть более читабельным. Существуют и другие объекты, и, например, AddStatusUpdate имеет некоторое дополнительное поведение - он фактически принимает имя категории статуса, и, если эта категория не существует, категория создается.
RJB
@RJB Как я уже сказал, AddStatusUpdate - это код, который использует домен. Так что либо какой-то веб-сервис или приложение, которое использует классы домена. И, как я сказал, вы не можете ожидать какой-либо макет или псевдокод, потому что вам нужно будет сделать весь проект достаточно большой сложности, чтобы продемонстрировать реальное преимущество модели ООП домена.
Эйфорическая
5

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

Независимо от вашего мнения об анемичной модели предметной области, стандартные шаблоны и методы, типичные для приложения Line of Business .NET, поддерживают многоуровневый подход на основе транзакций, состоящий из различных компонентов. Они поощряют отделение бизнес-логики от модели предметной области, в частности, для облегчения связи модели общей предметной области между другими компонентами .NET, а также с компонентами в различных технологических стеках или между физическими уровнями.

Одним из примеров этого может быть веб-служба SOAP на основе .NET, которая взаимодействует с клиентским приложением Silverlight, в котором есть библиотека DLL, содержащая простые типы данных. Этот проект сущности домена может быть встроен в сборку .NET или сборку Silverlight, где заинтересованные компоненты Silverlight, имеющие эту DLL, не будут подвергаться поведению объектов, которое может зависеть от компонентов, доступных только службе.

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

maple_shaft
источник
1
1) Как вы можете отделить бизнес-логику от модели предметной области? Это область, в которой живет эта бизнес-логика; сущности в этом домене выполняют поведение, связанное с этой бизнес-логикой. В реальном мире нет услуг, и они не существуют в головах экспертов по предметной области. 2) Любой компонент, который хочет интегрироваться с вами, должен создать свою собственную модель домена, потому что его потребности будут отличаться, и у него будет другое представление о вашей модели домена. Это давняя ошибка, когда вы можете создать одну модель предметной области, которой можно обмениваться.
Стефан Биллиет
1
@StefanBilliet Это хорошие моменты относительно ошибки универсальной доменной модели, но это возможно в более простых компонентах и ​​взаимодействии компонентов, как я делал это раньше. Мое мнение таково, что логика перевода между моделями доменов может привести к большому количеству утомительного и стандартного кода, и, если этого можно избежать безопасно, то это может быть хорошим выбором при проектировании.
maple_shaft
1
Честно говоря, я думаю, что единственный хороший выбор дизайна - это модель, о которой может подумать бизнес-эксперт. Вы строите модель домена для бизнеса, чтобы использовать его для решения определенных проблем в этом домене. Разделение поведения от доменных сущностей на сервисы усложняет задачу для всех участников, потому что вам постоянно приходится сопоставлять то, что говорят эксперты домена, с сервисным кодом, который практически не похож на текущий разговор. По моему опыту, вы теряете при этом гораздо больше времени, чем набор шаблонов. Это не значит, что нет никаких способов обойти кодекс котельной.
Стефан Биллиет
@StefanBilliet В идеальном мире я согласен с вами, когда у бизнес-эксперта есть время, чтобы посидеть с разработчиками. Реальность индустрии программного обеспечения заключается в том, что у бизнес-эксперта нет времени или интереса участвовать в этом уровне или даже хуже, но от разработчиков ожидается, что он поймет это только с расплывчатым руководством.
maple_shaft
Правда, но это не повод принять эту реальность. Продолжать в этом стремлении означает тратить время (и, возможно, репутацию) разработчиков и деньги заказчика. Процесс, который я описал, - это отношения, которые нужно строить со временем; это требует больших усилий, но дает гораздо лучшие результаты. Есть причина, по которой «вездесущий язык» часто считается наиболее важным аспектом DDD.
Стефан Биллиет
5

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

Инкапсулируйте «изменение статуса рабочего элемента» в WorkItemклассе следующим образом:

public SomeStatusUpdateType Status { get; private set; }

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Maybe we designed this badly at first ;-)
    Status = status;       
}

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

Мы изменим это на что-то вроде этого:

private ICollection<SomeStatusUpdateType> StatusUpdates { get; private set; }
public SomeStatusUpdateType Status => StatusUpdates.OrderByDescending(s => s.CreatedOn).FirstOrDefault();

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Better...
    StatusUpdates.Add(status);       
}

Реализация претерпела радикальные изменения, но вызывающий ChangeStatusметод не знает подробностей базовой реализации и не имеет оснований для изменения самого себя.

Это пример объекта модели расширенного домена, ИМХО.

дон
источник