Модель Rich vs Anemic Domain [закрыто]

99

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

Я создавал веб-приложения с использованием модели анемичного домена, поддерживаемой системой уровня Service -> Repository -> Storage , используя FluentValidation для проверки BL и помещая все мои BL на уровень Service.

Я прочитал книгу DDD Эрика Эвана, и он (вместе с Фаулером и другими), похоже, считает, что модели анемической области - это антипаттерн.

Так что мне просто очень хотелось разобраться в этой проблеме.

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

Сэм
источник
Вы также можете проверить этот блог , который говорит в пользу анемии модели предметной области
Иафет Ongeri - inkalimeva
14
DDD> ADM , ADM> DDD , DDD> ADM , ADM> DDD , ADM + DDD ... DDD / ADM, или как не договориться о дизайне программного обеспечения !
sp00m
Вот пример того, как избежать анемичной модели домена: medium.com/@wrong.about/…
Вадим Самохин
11
Забавно, что на этот вопрос можно было ответить с помощью одной ссылки на реальный проект, финансируемый реальной организацией. Через 5 лет нет хорошего ответа, ИМО. Обсуждение дешево. Покажи мне код.
Mateusz

Ответы:

59

Разница в том, что анемичная модель отделяет логику от данных. Логика часто помещаются в классах по имени **Service, **Util, **Manager, **Helperи так далее. Эти классы реализуют логику интерпретации данных и поэтому принимают модель данных в качестве аргумента. Например

public BigDecimal calculateTotal(Order order){
...
}

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

order.getTotal();

Это имеет большое влияние на целостность объекта. Поскольку логика интерпретации данных обертывает данные (доступ к данным возможен только через методы объекта), методы могут реагировать на изменения состояния других данных -> Это то, что мы называем поведением.

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

Для более глубокого понимания загляните в мой блог https://www.link-intersystems.com/blog/2011/10/01/anemic-vs-rich-domain-models/

Рене Линк
источник
15
Скажем, расчет общей стоимости заказа включает в себя: 1) Применение скидки, которая зависит от того, является ли клиент участником одной из возможных программ лояльности. 2) Применение скидки для заказов, которые содержат определенную группу товаров вместе, в зависимости от текущей маркетинговой кампании, проводимой магазином. 3) Расчет налога, где сумма налога зависит от каждого конкретного пункта заказа. На ваш взгляд, где могла бы быть вся эта логика? Не могли бы вы привести простой пример псевдокода. Спасибо!
Nik
4
@Nik В расширенной модели Order будет иметь ссылку на объект Customer, а объект Customer будет иметь ссылку на программу лояльности. Таким образом, Орден будет иметь доступ ко всей необходимой информации, не требуя явных ссылок на такие вещи, как службы и репозитории, из которых можно получить эту информацию. Однако кажется, что легко столкнуться с циклическими ссылками. Т.е. заказ ссылается на Заказчика, у Заказчика есть список всех Заказов. Я думаю, что отчасти поэтому люди сейчас предпочитают Анемик.
давка
3
@crush Подход, который вы описываете, работает очень хорошо. Есть одна загвоздка. Скорее всего, мы храним свои сущности в БД. Итак, чтобы рассчитать общую сумму заказа, мы должны получить из БД данные о заказе, клиенте, программе лояльности, маркетинговой кампании и таблице налогов. Учтите также, что у клиента есть коллекция заказов, у программы лояльности есть коллекция клиентов и так далее. Если мы наивно получим все это, мы в конечном итоге загрузим всю БД в ОЗУ. Это, конечно, нежизнеспособно, поэтому мы прибегаем к загрузке только релевантных данных из БД ... 1/2
Ник
3
@Nik "Если мы получим все это изначально, мы загрузим всю БД в ОЗУ". На мой взгляд, это один из главных недостатков богатой модели. Богатая модель хороша, пока ваш домен не станет большим и сложным, а затем вы начнете сталкиваться с ограничениями инфраструктуры. Но именно здесь на помощь могут прийти ORM с отложенной загрузкой. Найдите хороший, и вы можете сохранить богатые модели, не загружая всю БД в память, когда вам нужна только 1/20 ее. Тем не менее, я склонен сам использовать анемическую модель с CQRS после многих лет, когда я мечтал от анемии к богатству.
давка
2
Еще одна вещь, которую следует учитывать, - это где живет логика вашей бизнес-области. Все больше и больше разработчиков переносят его из базы данных в приложения, которым, на мой взгляд, он принадлежит. Но если вы застряли в ситуации, когда ваша компания требует, чтобы бизнес-логика оставалась на уровне базы данных (хранимых процедурах), то вы почти наверняка не выиграете от добавления этой логики в расширенную модель предметной области. На самом деле, вы просто могли бы настраиваете себя бежать в конфликты , где хранимые процедуры имеют разные правила , чем домен слой вашего приложения ...
раздавить
54

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

Вот краткое изложение, которое он представляет:

  • объекты домена не должны управляться Spring (IoC), в них не должно быть внедренных DAO или чего-либо, связанного с инфраструктурой

  • объекты домена имеют объекты домена, от которых они зависят, установленные спящим режимом (или механизмом сохранения)

  • объекты домена выполняют бизнес-логику, как и основная идея DDD, но это не включает запросы к базе данных или CRUD - только операции над внутренним состоянием объекта

  • DTO нужны редко - объекты домена в большинстве случаев сами являются DTO (что позволяет сэкономить некоторый шаблонный код)

  • службы выполняют операции CRUD, отправляют электронные письма, координируют объекты домена, генерируют отчеты на основе нескольких объектов домена, выполняют запросы и т. д.

  • уровень сервиса (приложения) не такой тонкий, но не включает бизнес-правила, присущие объектам домена

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

ОБНОВИТЬ

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

гео и
источник
11
Я не могу извлечь из этой статьи, что Божо, кажется, отстаивает модель анемичной области. уровень службы (приложения) не такой тонкий, но он не включает бизнес-правила, присущие объектам домена . Я понимаю, что объекты домена должны содержать внутреннюю бизнес-логику, но они не должны содержать никакой другой логики инфраструктуры . Мне этот подход совсем не кажется анемичной моделью предметной области.
Utku
8
Также это: объекты домена выполняют бизнес-логику, как и основная идея DDD, но она не включает запросы к базе данных или операции только с CRUD над внутренним состоянием объекта . Эти утверждения, похоже, совсем не в пользу модели анемичной области. Они только заявляют, что логика инфраструктуры не должна быть связана с объектами домена. По крайней мере, я так понимаю.
Utku
@Utku На мой взгляд, кажется довольно очевидным, что Божо выступает за своего рода гибрид между двумя моделями, гибрид, который, я бы сказал, ближе к анемичной модели, чем к богатой.
geoand
41

Моя точка зрения такова:

Модель анемичного домена = таблицы базы данных, сопоставленные с объектами (только значения полей, без реального поведения)

Богатая модель предметной области = набор объектов, демонстрирующих поведение

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

* Обратите внимание, что поведение объекта не имеет ничего общего с настойчивостью. Другой уровень (Data Mappers, Repositories и т.д.) отвечает за сохранение объектов домена.

Джордж
источник
5
Извините за мое незнание, но как модель богатой предметной области может следовать принципу SOLID, если вы поместите всю логику, связанную с Entity, в класс. Это нарушает принцип SOLID, в точности букву «S», которая означает единую ответственность, которая гласит, что класс должен делать только одно и делать это правильно.
redigaffi
6
@redigaffi Это зависит от того, как вы определяете «одно». Рассмотрим класс с двумя свойствами и двумя методами: x, y, sumи difference. Это четыре вещи. Или вы можете возразить, что это сложение и вычитание (две вещи). Или вы можете возразить, что это математика (одно). В блогах есть много сообщений о том, как найти баланс при применении SRP. Вот один: hackernoon.com/…
Rainbolt
2
В DDD единственная ответственность означает, что класс / модель может управлять своим собственным состоянием, не вызывая никаких побочных эффектов для остальной части системы в целом. По моему опыту, любое другое определение просто приводит к утомительным философским спорам.
ZombieTfk 06
12

Прежде всего, я скопировал ответ из этой статьи http://msdn.microsoft.com/en-gb/magazine/dn385704.aspx

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

Figure 1 Typical Anemic Domain Model Classes Look Like Database Tables

public class Customer : Person
{
  public Customer()
  {
    Orders = new List<Order>();
  }
  public ICollection<Order> Orders { get; set; }
  public string SalesPersonId { get; set; }
  public ShippingAddress ShippingAddress { get; set; }
}
public abstract class Person
{
  public int Id { get; set; }
  public string Title { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string CompanyName { get; set; }
  public string EmailAddress { get; set; }
  public string Phone { get; set; }
}

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

Figure 2 A Customer Type That’s a Rich Domain Model, Not Simply Properties

public class Customer : Contact
{
  public Customer(string firstName, string lastName, string email)
  {
    FullName = new FullName(firstName, lastName);
    EmailAddress = email;
    Status = CustomerStatus.Silver;
  }
  internal Customer()
  {
  }
  public void UseBillingAddressForShippingAddress()
  {
    ShippingAddress = new Address(
      BillingAddress.Street1, BillingAddress.Street2,
      BillingAddress.City, BillingAddress.Region,
      BillingAddress.Country, BillingAddress.PostalCode);
  }
  public void CreateNewShippingAddress(string street1, string street2,
   string city, string region, string country, string postalCode)
  {
    ShippingAddress = new Address(
      street1,street2,
      city,region,
      country,postalCode)
  }
  public void CreateBillingInformation(string street1,string street2,
   string city,string region,string country, string postalCode,
   string creditcardNumber, string bankName)
  {
    BillingAddress = new Address      (street1,street2, city,region,country,postalCode );
    CreditCard = new CustomerCreditCard (bankName, creditcardNumber );
  }
  public void SetCustomerContactDetails
   (string email, string phone, string companyName)
  {
    EmailAddress = email;
    Phone = phone;
    CompanyName = companyName;
  }
  public string SalesPersonId { get; private set; }
  public CustomerStatus Status { get; private set; }
  public Address ShippingAddress { get; private set; }
  public Address BillingAddress { get; private set; }
  public CustomerCreditCard CreditCard { get; private set; }
}
Разан Пол
источник
2
Проблема с методами, которые создают объект и присваивают свойство вновь созданному объекту. Они делают код менее расширяемым и гибким. 1) Что делать, если потребитель кода хочет создать не Address, а ExtendedAddressунаследованный от Address, с несколькими дополнительными свойствами? 2) Или изменить CustomerCreditCardпараметры конструктора, чтобы взять BankIDвместо BankName?
Лайтман,
Что для создания адреса требует дополнительных услуг, чем то, что составляет объект? Вам остается внедрить метод, чтобы получить эти услуги. Что делать, если услуг много?
давка
8

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

Пример классов предметной области с поведением:

class Order {

     String number

     List<OrderItem> items

     ItemList bonus

     Delivery delivery

     void addItem(Item item) { // add bonus if necessary }

     ItemList needToDeliver() { // items + bonus }

     void deliver() {
         delivery = new Delivery()
         delivery.items = needToDeliver()
     }

}

Метод needToDeliver()вернет список предметов, которые необходимо доставить, включая бонус. Его можно вызвать внутри класса, из другого связанного класса или из другого уровня. Например, если вы перейдете Orderк просмотру, то вы можете использовать needToDeliver()selected Orderдля отображения списка элементов, которые должны быть подтверждены пользователем, прежде чем они нажмут кнопку сохранения, чтобы сохранитьOrder .

Ответ на комментарий

Вот как я использую класс домена из контроллера:

def save = {
   Order order = new Order()
   order.addItem(new Item())
   order.addItem(new Item())
   repository.create(order)
}

Создание Orderи LineItemэто в одной транзакции. Если один из них LineItemне может быть создан, не Orderбудет создан.

Я предпочитаю использовать метод, представляющий одну транзакцию, например:

def deliver = {
   Order order = repository.findOrderByNumber('ORDER-1')
   order.deliver()       
   // save order if necessary
}

Что-нибудь внутри deliver() будет выполнено как одна транзакция. Если мне нужно выполнить много несвязанных методов в одной транзакции, я бы создал класс обслуживания.

Чтобы избежать исключения из ленивой загрузки, я использую именованный граф сущностей JPA 2.1. Например, в контроллере экрана доставки я могу создать метод для загрузкиdelivery атрибута и игнорирования bonus, например repository.findOrderByNumberFetchDelivery(). На бонусном экране я вызываю другой метод, который загружает bonusатрибут и игнорирует его delivery, например repository.findOrderByNumberFetchBonus(). Это требует дисциплины, так как я все еще не могу позвонить deliver()на бонусный экран.

Jocki
источник
1
Как насчет объема транзакции?
kboom
5
Поведение модели предметной области не должно содержать логики сохранения (включая транзакцию). Они должны быть тестируемыми (в модульном тесте) без подключения к базе данных. За область транзакции отвечает уровень обслуживания или уровень сохраняемости.
jocki
1
Как насчет ленивой загрузки?
kboom
Когда вы создаете экземпляры классов домена в модульном тесте, они не находятся в управляемом состоянии, потому что являются простыми объектами. Любое поведение можно проверить должным образом.
jocki
А что происходит, когда вы ожидаете объект домена от уровня сервиса? Разве это не удалось?
kboom
8

Когда я писал монолитные настольные приложения, я создавал богатые модели предметной области, и мне нравилось их создавать.

Теперь я пишу крошечные микросервисы HTTP, в них как можно меньше кода, включая анемичные DTO.

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

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

Микросервис заказов может иметь очень мало функций, выраженных как ресурсы RESTful, через SOAP или что-то еще. Код микросервиса заказов может быть предельно простым.

Более крупный, более монолитный одиночный (микро) сервис, особенно тот, который хранит его модель в ОЗУ, может выиграть от DDD.

Люк Пуплетт
источник
Есть ли у вас какие-либо примеры кода для микросервисов HTTP, которые отражают ваше текущее состояние дел? Не просите вас ничего писать, просто поделитесь ссылками, если у вас есть что-то, на что вы могли бы указать. Спасибо.
Кейси Пламмер
3

Я думаю, что корень проблемы в ложной дихотомии. Как можно выделить эти две модели: богатую и «анемичную» и противопоставить их друг другу? Я думаю, что это возможно, только если у вас неправильное представление о том, что такое класс . Не уверен, но мне кажется, что я нашел это в одном из видео Божидара Божанова на Youtube. Класс - это не данные + методы над этими данными. Это совершенно неверное понимание, которое приводит к разделению классов на две категории: только данные, поэтому анемичная модель и данные + методы - настолько богатая модель (вернее, есть третья категория: даже только методы).

Верно то, что класс - это понятие в какой-то онтологической модели, слово, определение, термин, идея, это ОБОЗНАЧЕНИЕ. . И это понимание устраняет ложную дихотомию: у вас не может быть ТОЛЬКО анемичной модели или ТОЛЬКО богатой модели, потому что это означает, что ваша модель неадекватна, она не имеет отношения к реальности: некоторые концепции имеют только данные, некоторые из них имеют только методы, некоторые из них смешанные. Поскольку в данном случае мы пытаемся описать некоторые категории, наборы объектов, отношения, концепции с помощью классов, и, как мы знаем, некоторые концепции являются только процессами (методами), некоторые из них являются только наборами атрибутов (данных), некоторые из них это отношения с атрибутами (смешанные).

Я считаю, что адекватное приложение должно включать все виды классов и избегать фанатичного самоограничения только одной моделью. Независимо от того, как логика представляет: с кодом или с интерпретируемыми объектами данных (например, Free Monads ), в любом случае: у нас должны быть классы (концепции, денотаты), представляющие процессы, логику, отношения, атрибуты, функции, данные и т. Д., А не попытаться избежать некоторых из них или свести их всех только к одному виду.

Итак, мы можем извлечь логику в другой класс и оставить данные в исходном, но это не имеет смысла, потому что некоторая концепция может включать атрибуты и отношения / процессы / методы, и их разделение будет дублировать концепцию под двумя именами, которые могут быть сводится к шаблонам: «ОБЪЕКТ-Атрибуты» и «ОБЪЕКТ-Логика». Это нормально для процедурных и функциональных языков из-за их ограничений. но это чрезмерное самоограничение для языка, который позволяет вам описывать все виды концепций.

RandomB
источник
1

Анемичные доменные модели важны для ORM и простой передачи по сетям (жизненная сила всех коммерческих приложений), но объектно-ориентированный подход очень важен для инкапсуляции и упрощения «транзакционных / обрабатывающих» частей вашего кода.

Поэтому важно уметь идентифицировать и преобразовывать из одного мира в другой.

Назовите модели Anemic как-то вроде AnemicUser или UserDAO и т. Д., Чтобы разработчики знали, что есть лучший класс для использования, а затем создайте соответствующий конструктор для класса none Anemic.

User(AnemicUser au)

и метод адаптера для создания анемичного класса для транспортировки / сохранения

User::ToAnemicUser() 

Стремитесь использовать неанемичного пользователя везде, кроме транспорта / постоянства

Эндрю Пэйт
источник
-1

Вот пример, который может помочь:

Анемичный

class Box
{
    public int Height { get; set; }
    public int Width { get; set; }
}

Без анемии

class Box
{
    public int Height { get; private set; }
    public int Width { get; private set; }

    public Box(int height, int width)
    {
        if (height <= 0) {
            throw new ArgumentOutOfRangeException(nameof(height));
        }
        if (width <= 0) {
            throw new ArgumentOutOfRangeException(nameof(width));
        }
        Height = height;
        Width = width;
    }

    public int area()
    {
       return Height * Width;
    }
}
Алиреза Рахмани Халили
источник
Похоже, его можно преобразовать в ValueObject против Entity.
code5 02
Просто копия вставки из Википедии без каких - либо объяснений
WST
кто раньше написал? @wst
Алиреза Рахмани Халили
@AlirezaRahmaniKhalili, согласно истории Википедии, они были первыми ... Если только я не понял ваш вопрос.
wst
-1

Классический подход к DDD не требует любой ценой избегать использования анемичных и богатых моделей. Однако MDA может по-прежнему применять все концепции DDD (ограниченные контексты, контекстные карты, объекты значений и т. Д.), Но во всех случаях использовать модели Anemic vs Rich. Во многих случаях использование доменных служб для оркестровки сложных вариантов использования домена в наборе агрегатов домена является гораздо лучшим подходом, чем простой вызов агрегатов из уровня приложения. Единственное отличие от классического подхода DDD в том, где находятся все проверки и бизнес-правила? Есть новая конструкция, известная как валидаторы моделей. Валидаторы гарантируют целостность полной входной модели до того, как будет реализован какой-либо вариант использования или рабочий процесс домена. Совокупные корневые и дочерние сущности анемичны, но каждый может иметь свои собственные валидаторы модели, вызываемые при необходимости, его корневым валидатором. Валидаторы по-прежнему придерживаются SRP, просты в обслуживании и поддаются модульному тестированию.

Причина этого сдвига в том, что сейчас мы больше двигаемся к подходу к микросервисам сначала API, а не UX. REST сыграл в этом очень важную роль. Традиционный подход к API (из-за SOAP) изначально был основан на командном API и HTTP-глаголах (POST, PUT, PATCH, GET и DELETE). API на основе команд хорошо сочетается с объектно-ориентированным подходом Rich Model и все еще очень актуален. Однако простые API-интерфейсы на основе CRUD, хотя они могут вписаться в расширенную модель, гораздо лучше подходят для простых анемичных моделей, валидаторов и доменных служб для организации всего остального.

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

code5
источник