Модель багатого та анемічного доменів [закрито]


97

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

Я створював веб-додатки з використанням Anemic Domain Model, підкріпленої Службою -> Репозиторій -> Система рівня зберігання , використовуючи FluentValidation для перевірки BL та розміщуючи всі свої BL на рівні Service.

Я прочитав книгу Еріка Евана про DDD, і він (разом із Фаулером та іншими), здається, вважає, що моделі анемічного домену є анти-зразком.

Тож я просто дуже хотів трохи зрозуміти цю проблему.

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



14
DDD> ADM , ADM> DDD , DDD> ADM , ADM> DDD , ADM + DDD ... DDD / ADM, або як не домовитись про розробку програмного забезпечення !
sp00m

Ось приклад того, як уникнути анемічної моделі домену: medium.com/@wrong.about/…
Вадим Самохін

11
Забавно, що на це питання можна було відповісти одним посиланням на реальний проект, що фінансується реальною організацією. Через 5 років хорошої відповіді немає, ІМО. Розмова дешева. Покажи мені код.
Матеуш Стефек,

Відповіді:


57

Різниця полягає в тому, що анемічна модель відокремлює логіку від даних. Логіка часто поміщаються в класах по імені **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) Розрахунок податку, де сума податку залежить від кожного конкретного пункту замовлення. На вашу думку, куди би належала вся ця логіка? Не могли б ви навести простий приклад псевдокоду. Дякую!
Нік

4
@Nik У багатофункціональній моделі Замовлення матиме посилання на об'єкт Клієнта, а об'єкт Клієнта - на Програму лояльності. Таким чином, Орден мав би доступ до всієї необхідної інформації, не потребуючи явних посилань на такі речі, як послуги та сховища, з яких можна отримати цю інформацію. Однак, здається, легко натрапити на випадок, коли відбуваються циклічні посилання. Тобто посилання на замовлення Клієнт, Клієнт має перелік усіх замовлень.
розчавити

3
@crush Підхід, який ви описуєте, працює дуже добре. Є один улов. Ймовірно, ми зберігаємо сутності в БД. Отже, для обчислення загальної суми замовлення ми повинні отримати з БД таблицю замовлення, клієнта, програму лояльності, маркетингову кампанію та податки. Враховуйте також, що Клієнт має колекцію Замовлень, Програма лояльності має колекцію Клієнтів тощо. Якщо ми наївно отримаємо все це, ми в підсумку завантажимо всю БД в оперативну пам’ять. Звичайно, це нежиттєздатно, тому ми вдаємось до завантаження лише відповідних даних з БД ... 1/2
Нік

3
@Nik "Якщо ми спочатку отримаємо все це, ми в підсумку завантажимо всю БД в оперативну пам'ять." Це один із головних недоліків багатої моделі на мій погляд. Багата модель приємна, поки ваш домен не стане великим та складним, а потім ви почнете вражати обмеження інфраструктури. Ось тут можуть допомогти ORM з ледачим навантаженням. Знайдіть хорошу, і ви зможете зберегти багаті моделі, не завантажуючи всю пам’ять БД у пам’ять, коли вам знадобилася лише 1/20 її частини. Тим не менше, я, як правило, використовую анемічну модель із CQRS після багатьох років пересування між анемічними та багатими.
розчавити

2
Ще слід врахувати, де живе ваша логіка бізнес-домену. Все більше і більше розробників переносять його з бази даних, і на додатки, де він належить, на мій погляд. Але якщо ви застрягли в ситуації, коли ваша компанія вимагає, щоб ділова логіка залишалася на рівні бази даних (збережені процедури), то ви майже напевно не виграєте від того, що додасте цю логіку в багату модель домену. Насправді ви могли просто налаштувати себе на конфлікти, коли збережені процедури мають інші правила, ніж рівень домену вашої програми ...
розчавити

53

Божидар Божанов, схоже, аргументує користь анемічної моделі в цьому дописі в блозі.

Ось короткий зміст, який він представляє:

  • об’єкти домену не повинні керуватися весною (IoC), вони не повинні мати DAO чи щось інше, пов’язане з інфраструктурою, що вводиться в них

  • об’єкти домену мають об’єкти домену, від яких вони залежать, встановлені сплячим режимом (або механізмом збереження)

  • доменні об'єкти виконують бізнес-логіку, як і є основною ідеєю DDD, але це не включає запити до бази даних або CRUD - лише операції з внутрішнім станом об'єкта

  • DTO рідко потрібні - об’єкти домену в більшості випадків є самими DTO (що зберігає деякий шаблонний код)

  • служби виконують операції CRUD, надсилають електронні листи, координують об'єкти домену, генерують звіти на основі кількох об'єктів домену, виконують запити тощо.

  • рівень служби (програми) не такий тонкий, але не включає ділові правила, які є суттєвими для об'єктів домену

  • слід уникати генерації коду. Абстракцію, шаблони дизайну та DI слід використовувати для подолання потреби у генерації коду і, зрештою, для позбавлення від дублювання коду.

ОНОВЛЕННЯ

Нещодавно я прочитав цю статтю, де автор виступає за дотримання свого роду гібридного підходу - об'єкти домену можуть відповідати на різні запитання, виходячи виключно зі свого стану (що у випадку абсолютно анемічних моделей, можливо, було б зроблено на рівні обслуговування)


11
Я не можу витягти з цієї статті, що Божо, схоже, аргументує користь моделі анемічного домену. рівень служби (програми) не такий тонкий, але не включає ділові правила, які є властивими об'єктам домену . Я розумію, що об’єкти домену повинні містити невід’ємну для них бізнес-логіку, але вони не повинні містити жодної іншої логіки інфраструктури . Такий підхід мені взагалі не здається анемічною моделлю домену.
Utku

8
Також цей: об’єкти домену виконують ділову логіку, якою є основна ідея DDD, але це не включає запити до бази даних або CRUD - лише операції з внутрішнім станом об’єкта . Схоже, ці твердження не сприяють моделі анемічного домену. Вони лише заявляють, що логіку інфраструктури не слід поєднувати з об'єктами домену. Принаймні це те, що я розумію.
Utku

@Utku На мій погляд, здається досить зрозумілим, що Божо виступає за своєрідний гібрид між двома моделями, гібрид, який, я б сказав, ближчий до анемічної моделі, ніж багата модель.
геоі

41

Моя точка зору така:

Анемічна модель домену = таблиці бази даних, зіставлені з об'єктами (лише значення полів, відсутність реальної поведінки)

Модель багатого домену = сукупність об'єктів, що виявляють поведінку

Якщо ви хочете створити простий CRUD-додаток, можливо, досить анемічної моделі з класичним фреймворком MVC. Але якщо ви хочете реалізувати якусь логіку, анемічна модель означає, що ви не будете робити об’єктно-орієнтоване програмування.

* Зверніть увагу, що поведінка об'єкта не має нічого спільного з наполегливістю. Інший рівень (Data Mappers, Repositories тощо) відповідає за збереження об’єктів домену.


5
Вибачте за моє незнання, але як модель багатого домену може слідувати принципу SOLID, якщо ви покладете всю логіку, пов'язану з Entity, у клас. Це порушує принцип SOLID, буква S, що означає єдину відповідальність, тобто клас повинен робити лише одне і робити це правильно.
redigaffi

6
@redigaffi Це залежить від того, як ви визначаєте "одне". Розглянемо клас з двома властивостями і двома методами: x, y, sumі difference. Це чотири речі. Або ви можете стверджувати, що це додавання та віднімання (дві речі). Або ви можете стверджувати, що це математика (одне). Є багато публікацій у блозі про те, як ви знаходите баланс у застосуванні SRP. Ось один: hackernoon.com/…
Рейнболт

2
У DDD одна відповідальність означає, що клас / модель може управляти своїм станом, не викликаючи побічних ефектів для решти системи в цілому. Будь-яке інше визначення просто призводить до нудних філософських суперечок на моєму досвіді.
ZombieTfk

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; }
}

У цій багатшій моделі, а не просто виставляти властивості, які слід прочитати та записати, публічна поверхня Клієнта складається з явних методів.

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?
Lightman

Що для створення адреси вимагає додаткових послуг, ніж те, що складає об'єкт? Вам залишається введення методу, щоб отримати ці послуги. Що робити, якщо це багато послуг?
розчавити

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()вибраний 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(). Для цього потрібен dicipline, оскільки я все ще не можу зателефонувати deliver()на бонусний екран.


1
Як щодо обсягу транзакції?
kboom

5
Поведінка моделі домену не повинна містити логіку постійності (включаючи транзакції). Вони повинні перевірятися (в модульному тесті) без підключення до бази даних. За обсяг транзакцій відповідає рівень обслуговування або рівень стійкості.
jocki

1
А як щодо лінивого завантаження?
kboom

Коли ви створюєте екземпляри класів домену в модульному тесті, вони не перебувають у керованому стані, оскільки вони є звичайними об'єктами. Усі способи поведінки можна перевірити належним чином.
jocki

А що відбувається, коли ви очікуєте об’єкт домену від сервісного рівня? Чи не вдається тоді?
kboom

8

Коли я писав монолітні настільні програми, я створював багаті моделі доменів, я із задоволенням їх створював.

Зараз я пишу крихітні мікросервіси HTTP, коду якомога менше, включаючи анемічні DTO.

Думаю, DDD та цей анемічний аргумент датуються епохою монолітного робочого столу чи сервера. Я пам’ятаю ту епоху і погодився б, що анемічні моделі дивні. Я створив великий монолітний додаток для торгівлі іноземною валютою, і жодної моделі не було, насправді, це було жахливо.

За допомогою мікросервісів, малі служби з їхньою багатою поведінкою, можливо, є складовими моделями та сукупностями в межах домену. Отже, самі реалізації мікросервісу можуть не вимагати подальшого DDD. Домен може бути додатком мікросервісу.

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

Більша, більш монолітна одиночна (мікро) послуга, особливо та, яка зберігає її модель в оперативній пам'яті, може отримати вигоду від DDD.


Чи є у вас приклади коду для мікросервісів HTTP, який відображає ваш сучасний стан техніки? Не просячи вас щось писати, просто поділіться посиланнями, якщо у вас є щось, на що ви могли б вказати. Дякую.
Кейсі Пламмер

3

Я думаю, що корінь проблеми полягає в хибній роздвоєності. Як можна витягти ці 2 моделі: багату та «анемічну» та порівняти їх між собою? Я думаю, що це можливо, лише якщо ви неправильно уявляєте, що таке клас . Я не впевнений, але, думаю, знайшов це в одному з відео Божидара Божанова на Youtube. Клас - це не дані + методи над цими даними. Це абсолютно невірне розуміння, що призводить до поділу класів на дві категорії: лише дані, тому анемічна модель та дані + методи - така багата модель (якщо бути точніше, існує 3-та категорія: навіть парні методи).

Істина полягає в тому, що клас - це поняття в якійсь онтологічній моделі, слово, визначення, термін, ідея, це ДЕНОТАТ . І це розуміння усуває помилкову дихотомію: ви не можете мати ТІЛЬКИ анемічну модель або ТІЛЬКИ багату модель, оскільки це означає, що ваша модель не є адекватною, вона не стосується реальності: деякі концепції мають лише дані, деякі з них мають лише методи, деякі з них змішані. Оскільки ми намагаємось описати, в даному випадку, деякі категорії, набори об'єктів, відносини, поняття з класами, і, як ми знаємо, деякі поняття є лише процесами (методами), деякі з них є набором лише атрибутів (дані), деякі з ними є відносини з атрибутами (змішані).

Я думаю, що адекватна програма повинна включати всі види занять і уникати фанатичного самообмеження лише однією моделлю. Незалежно від того, як представлена ​​логіка: з кодом або з інтерпретованими об'єктами даних (наприклад, Free Monads ), у будь-якому випадку: ми повинні мати класи (концепції, денотати), що представляють процеси, логіку, відносини, атрибути, особливості, дані тощо, а не намагатися уникати деяких з них або зводити всіх до єдиного виду.

Отже, ми можемо витягнути логіку до іншого класу та залишити дані у вихідному, але це не має сенсу, оскільки деяке поняття може включати атрибути та відношення / процеси / методи, і поділ їх призведе до дублювання концепції під 2 іменами, які можуть бути зведено до шаблонів: "OBJECT-Атрибути" та "OBJECT-Logic". Це добре в процедурних та функціональних мовах через їх обмеженість, але це надмірне самообмеження для мови, що дозволяє описувати всі види понять.


1

Анемічні моделі доменів важливі для ORM і легкої передачі через мережі (життєва кров усіх комерційних додатків), але OO дуже важлива для інкапсуляції та спрощення частин коду, що стосуються транзакцій / обробки.

Тому важливим є можливість ідентифікації та перетворення з одного світу в інший.

Назвіть Anemic моделями щось на зразок AnemicUser, або UserDAO тощо, щоб розробники знали, що є кращий клас для використання, а потім мати відповідний конструктор для класу none Anemic

User(AnemicUser au)

і метод адаптера для створення анемічного класу для транспортування / персистенції

User::ToAnemicUser() 

Постарайтеся використовувати користувача Anemic скрізь за межами транспорту / постійності


-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

Просто копія з Вікіпедії без будь-яких пояснень
wst

хто писав раніше? @wst
Аліреза Рахмані Халілі

@AlirezaRahmaniKhalili згідно історії Вікіпедії, вони були першими ... Хіба що, я не зрозумів вашого запитання.
wst

-1

Класичний підхід до DDD не передбачає уникнення Anemic vs Rich Models за будь-яку ціну. Однак MDA все ще може застосовувати всі концепції DDD (обмежений контекст, карти контексту, об'єкти значень тощо), але у всіх випадках використовувати моделі Anemic vs Rich. Є багато випадків, коли використання доменних служб для організації складних випадків використання доменів через набір агрегатів доменів є набагато кращим підходом, ніж просто агрегати, що викликаються з рівня додатків. Єдина відмінність від класичного підходу DDD полягає в тому, де містяться всі перевірки та ділові правила? Існує нова конструкція, відома як валідатори моделей. Валідатори забезпечують цілісність повної вхідної моделі до того, як відбувається будь-який варіант використання чи робочий процес домену. Сукупні кореневі та дочірні сутності анемічні, але кожна може мати власні валідатори моделей, що викликаються за необхідності, за допомогою кореневого валідатора. Валідатори, як і раніше дотримуються SRP, прості в обслуговуванні та перевіряються.

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

Я люблю DDD у всьому, що він може запропонувати, але настає момент, коли вам потрібно трохи розтягнути його, щоб він відповідав постійним змінам та кращому підходу до архітектури.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.