Модель Rich and Anemic Domain
Я решаю, следует ли мне использовать модель расширенного домена над моделью анемичного домена, и ищу хорошие примеры этих двух.
Я строил веб-приложения с использованием модели Anemic Domain Model, опирающейся на систему уровня Service -> Repository -> Storage, используя FluentValidation для проверки BL и помещая весь свой BL в уровень Service.
Я прочитал книгу DDD Эрика Эвана, и он (вместе с Фаулером и другими), похоже, считает, что Анемичные Доменные Модели являются анти-паттерном.
Так что я просто очень хотел получить представление об этой проблеме.
Кроме того, я действительно ищу несколько хороших (базовых) примеров богатой модели предметной области и преимуществ по сравнению с моделью анемичной предметной области, которую она предоставляет.
8 ответов
Разница в том, что анемичная модель отделяет логику от данных. Логика часто размещается в классах с именем **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/
Божидар Божанов, кажется, выступает в пользу анемичной модели в этом сообщении в блоге.
Вот резюме, которое он представляет:
доменные объекты не должны управляться с помощью Spring (IoC), в них не должно быть DAO или чего-либо связанного с инфраструктурой
доменные объекты имеют объекты домена, от которых они зависят, установленные hibernate (или механизм персистентности)
Объектные домены выполняют бизнес-логику, как и основная идея DDD, но это не включает запросы к базе данных или CRUD - только операции над внутренним состоянием объекта
редко нужны DTO - доменные объекты в большинстве случаев сами являются DTO (что сохраняет некоторый шаблонный код)
службы выполняют операции CRUD, отправляют электронные письма, координируют объекты домена, генерируют отчеты на основе нескольких объектов домена, выполняют запросы и т. д.
уровень обслуживания (приложения) не так уж и мал, но не включает бизнес-правила, присущие объектам домена.
генерации кода следует избегать. Абстракция, шаблоны проектирования и DI должны использоваться для преодоления необходимости генерации кода и, в конечном итоге, для избавления от дублирования кода.
ОБНОВИТЬ
Недавно я читал эту статью, где автор выступает за использование своего рода гибридного подхода - доменные объекты могут отвечать на различные вопросы, основываясь исключительно на их состоянии (что в случае полностью анемичных моделей, вероятно, будет выполнено на уровне обслуживания)
Моя точка зрения такова:
Модель анемичного домена = таблицы базы данных, сопоставленные с объектами (только значения полей, без реального поведения)
Богатая модель предметной области = коллекция объектов, демонстрирующих поведение
Если вы хотите создать простое приложение CRUD, возможно, достаточно анемичной модели с классической средой MVC. Но если вы хотите реализовать какую-то логику, анемичная модель означает, что вы не будете заниматься объектно-ориентированным программированием.
* Обратите внимание, что поведение объекта не имеет ничего общего с постоянством. Другой слой (Data Mappers, Repositories и т. Д.) Отвечает за сохранение объектов домена.
Когда я писал монолитные настольные приложения, я создавал богатые доменные модели, которые мне нравились.
Сейчас я пишу крошечные микросервисы HTTP, кода как можно меньше, включая анемичные DTO.
Я думаю, что DDD и этот анемичный аргумент относятся к эпохе монолитных настольных или серверных приложений. Я помню ту эпоху, и я бы согласился, что анемичные модели странные. Я создал большое монолитное приложение для торговли на рынке Форекс, и не было никакой модели, правда, это было ужасно.
С микросервисами, небольшие сервисы с их богатым поведением, возможно, являются составными моделями и совокупностями в домене. Таким образом, сами реализации микросервисов могут не требовать дополнительного DDD. Приложение микросервиса может быть доменом.
Микросервис заказов может иметь очень мало функций, выраженных в виде ресурсов RESTful или через SOAP или что-либо еще. Код заказа микросервисов может быть предельно простым.
DDD может выиграть от более крупной, более монолитной (микро) службы, особенно той, которая поддерживает модель в ОЗУ.
Прежде всего, я скопировал и вставил ответ из этой статьи 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; }
}
Одним из преимуществ богатых классов домена является то, что вы можете вызывать их поведение (методы) каждый раз, когда у вас есть ссылка на объект в любом слое. Кроме того, вы, как правило, пишете небольшие и распределенные методы, которые работают вместе. В классах анемичных доменов вы склонны писать толстые процедурные методы (на уровне сервисов), которые обычно определяются сценарием использования. Они обычно менее ремонтопригодны по сравнению с богатыми классами доменов.
Пример классов домена с поведением:
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()
, Это требует участия, так как я до сих пор не могу позвонить deliver()
внутри бонусного экрана.
Я думаю, что корень проблемы в ложной дихотомии. Как можно выделить эти две модели: богатую и "анемичную" и противопоставить их друг другу? Я думаю, что это возможно только в том случае, если у вас неправильное представление о том, что такое класс. Я не уверен, но мне кажется, что я нашел его в одном из видео Божидара Божанова на Youtube. Класс - это не данные + методы над этими данными. Это совершенно неверное понимание, которое приводит к разделению классов на две категории: только данные, поэтому анемичная модель и данные + методы - такая богатая модель (вернее, есть третья категория: даже только методы).
Верно то, что класс - это понятие в какой-то онтологической модели, слово, определение, термин, идея, это ДЕНОТАТ. И это понимание устраняет ложную дихотомию: у вас не может быть ТОЛЬКО анемичной модели или ТОЛЬКО богатой модели, потому что это означает, что ваша модель неадекватна, она не имеет отношения к реальности: некоторые концепции имеют только данные, некоторые из них имеют только методы, некоторые из них смешанные. Поскольку в данном случае мы пытаемся описать некоторые категории, наборы объектов, отношения, концепции с помощью классов, и, как мы знаем, некоторые концепции являются только процессами (методами), некоторые из них представляют собой только наборы атрибутов (данные), некоторые из это отношения с атрибутами (смешанные).
Я считаю, что адекватное приложение должно включать все виды классов и избегать фанатичного самоограничения только одной моделью. Независимо от того, как логика представлена: с кодом или с интерпретируемыми объектами данных (например, Free Monads), в любом случае: у нас должны быть классы (концепции, денотаты), представляющие процессы, логику, отношения, атрибуты, функции, данные и т. Д., А не попытаться избежать некоторых из них или свести их всех только к одному виду.
Итак, мы можем извлечь логику в другой класс и оставить данные в исходном, но это не имеет смысла, потому что некоторая концепция может включать атрибуты и отношения / процессы / методы, и их разделение будет дублировать концепцию под двумя именами, которые могут быть сводится к шаблонам: "ОБЪЕКТ-Атрибуты" и "ОБЪЕКТ-Логика". Это нормально для процедурных и функциональных языков из-за их ограничений, но это чрезмерное самоограничение для языка, который позволяет вам описывать все виды концепций.
Модели анемичных доменов важны для ORM и легкого переноса по сетям (жизненная сила всех коммерческих приложений), но OO очень важен для инкапсуляции и упрощения "транзакционных / управляемых" частей вашего кода.
Поэтому важно иметь возможность идентифицировать и преобразовывать из одного мира в другой.
Имя Anemic моделирует что-то вроде AnemicUser или UserDAO и т. Д., Чтобы разработчики знали, что есть лучший класс для использования, а затем имеют соответствующий конструктор для класса без Anemic.
User(AnemicUser au)
и метод адаптера для создания анемичного класса для транспортировки / сохранения
User::ToAnemicUser()
Стремитесь использовать ни одного Anemic User везде вне транспорта / постоянства
Классический подход к 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 во всем, что он может предложить, но приходит время, когда вам нужно немного расширить его, чтобы он соответствовал постоянно меняющимся и лучшему подходу к архитектуре.
Вот пример, который может помочь:
малокровный
class Box
{
public int Height { get; set; }
public int Width { get; set; }
}
Non-анемия
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;
}
}