Проектирование, ориентированное на предметную область:Избегание анемичных областей и моделирование ролей в реальном мире
-
26-10-2019 - |
Вопрос
Я ищу несколько советов о том, насколько сильно мне следует беспокоиться о том, чтобы избегать анемичной доменной модели.Мы только начинаем работать с DDD и боремся с параличом анализа в отношении простых проектных решений.Последний момент, которого мы придерживаемся, - это то, к чему относится определенная бизнес-логика, например, у нас есть Order
объект, обладающий такими свойствами, как Status
и т.д.Теперь предположим, что я должен выполнить команду типа UndoLastStatus
поскольку кто-то допустил ошибку с заказом, это не так просто, как просто изменить Status
поскольку другая информация должна быть занесена в журнал и свойства изменены.Теперь, в реальном мире, это чисто административная задача.Итак, на мой взгляд, у меня есть два варианта, которые я могу придумать:
Вариант 1:Добавьте метод для заказа, чтобы что-то вроде
Order.UndoLastStatus()
, хотя это в некотором роде имеет смысл, на самом деле это не отражает предметную область.ТакжеOrder
является основным объектом в системе, и если все, что связано с order, будет помещено в класс order, ситуация может выйти из-под контроля.Вариант 2:Создать
Shop
объект, и при этом имеют разные службы, которые представляют разные роли.Так что я мог быShop.AdminService
,Shop.DispatchService
, иShop.InventoryService
.Так что в этом случае я бы имелShop.AdminService.UndoLastStatus(Order)
.
Теперь второй вариант: у нас есть нечто, что гораздо лучше отражает предметную область и позволило бы разработчикам поговорить с бизнес-экспертами о похожих ролях, которые действительно существуют.Но это также ведет к анемичной модели.Какой путь был бы лучшим в целом?
Решение
Вариант 2 наверняка приведет к процедурному коду.
Возможно, его проще разработать, но гораздо сложнее поддерживать.
Теперь, в реальном мире, это чисто административная задача
Задачи "Администрирования" должны быть частными и вызываться посредством публичных, полностью "доменных" действий.Предпочтительно - по-прежнему написанный простым для понимания кодом, который управляется из домена.
На мой взгляд, проблема в том, что UndoLastStatus
для эксперта в предметной области это имеет мало смысла.
Скорее всего, речь идет о создании, отмене и исполнении заказов.
Что-то в этом роде могло бы подойти лучше:
class Order{
void CancelOrder(){
Status=Status.Canceled;
}
void FillOrder(){
if(Status==Status.Canceled)
throw Exception();
Status=Status.Filled;
}
static void Make(){
return new Order();
}
void Order(){
Status=Status.Pending;
}
}
Лично мне не нравится использование "статусов", они автоматически передаются всем, кто их использует - я вижу это как ненужное соединение.
Так что у меня было бы что-то вроде этого:
class Order{
void CancelOrder(){
IsCanceled=true;
}
void FillOrder(){
if(IsCanceled) throw Exception();
IsFilled=true;
}
static Order Make(){
return new Order();
}
void Order(){
IsPending=true;
}
}
Для изменения связанных вещей при изменении состояния заказа лучше всего использовать так называемый события домена.
Мой код будет выглядеть примерно так:
class Order{
void CancelOrder(){
IsCanceled=true;
Raise(new Canceled(this));
}
//usage of nested classes for events is my homemade convention
class Canceled:Event<Order>{
void Canceled(Order order):base(order){}
}
}
class Customer{
private void BeHappy(){
Console.WriteLine("hooraay!");
}
//nb: nested class can see privates of Customer
class OnOrderCanceled:IEventHandler<Order.Canceled>{
void Handle(Order.Canceled e){
//caveat: this approach needs order->customer association
var order=e.Source;
order.Customer.BeHappy();
}
}
}
Если заказ становится слишком большим, возможно, вы захотите проверить, что ограниченные контексты являются (как говорит Эрик Эванс - если бы у него был шанс написать свою книгу снова, он бы перенес ограниченные контексты в самое начало).
Короче говоря, это форма декомпозиции, основанная на предметной области.
Идея относительно проста - это нормально иметь несколько Заказов с разных точек зрения, то есть контекстов.
Например.- Заказ из контекста покупки, Заказ из контекста учета.
namespace Shopping{
class Order{
//association with shopping cart
//might be vital for shopping but completely irrelevant for accounting
ShoppingCart Cart;
}
}
namespace Accounting{
class Order{
//something specific only to accounting
}
}
Но обычно достаточный домен сам по себе избегает сложности и легко поддается декомпозиции, если вы прислушаетесь к нему достаточно внимательно.Например.Вы можете услышать от экспертов такие термины, как OrderLifeCycle, OrderHistory, OrderDescription, которые вы можете использовать в качестве якорей для декомпозиции.
Примечание:Имейте в виду - я ничего не понимаю о Вашем домене.
Вполне вероятно, что те глаголы, которые я использую, совершенно незнакомы ему.
Другие советы
Я бы руководствовался следующим СХВАТИТЬ принципы.Примените Эксперт по информации принцип проектирования заключается в том, что вы должны возложить ответственность на класс, который, естественно, обладает наибольшим количеством информации, необходимой для выполнения изменения.
В этом случае, поскольку изменение статуса заказа связано с другими объектами, я бы сделал так, чтобы каждый из этих низкоуровневых объектов домена поддерживал метод для применения изменения по отношению к самому себе.Затем также используйте уровень обслуживания домена, как вы описали в варианте 2, который абстрагирует всю операцию, охватывая при необходимости несколько объектов домена.
Также смотрите Фасад закономерность.
Я думаю, что наличие такого метода, как UndoLastStatus, в классе Order кажется немного неправильным, потому что причины его существования в некотором смысле выходят за рамки order.С другой стороны, наличие метода, который отвечает за изменение статуса заказа, Order.ChangeStatus, прекрасно вписывается в модель предметной области.Статус заказа - это надлежащая концепция домена, и изменение этого статуса должно выполняться через класс Order, поскольку он владеет данными, связанными со статусом заказа - класс Order несет ответственность за поддержание своей согласованности и надлежащего состояния.
Другой способ думать об этом заключается в том, что объект Order - это то, что сохраняется в базе данных, и это "последняя остановка" для всех изменений, примененных к Заказу.Легче рассуждать о том, каким может быть допустимое состояние для заказа, с точки зрения Заказа, а не с точки зрения внешнего компонента.Именно в этом суть DDD и ООП: людям легче рассуждать о коде.Кроме того, для выполнения изменения состояния может потребоваться доступ к закрытым или защищенным элементам, и в этом случае лучшим вариантом является использование метода в классе order.Это одна из причин, по которой к анемичным моделям предметной области относятся неодобрительно - они перекладывают ответственность за поддержание согласованности состояния на класс-владельца, тем самым нарушая инкапсуляцию, среди прочего.
Одним из способов реализации более специфичной операции, такой как UndoLastStatus, было бы создание OrderService, который предоставляет доступ к домену и показывает, как внешние компоненты работают с доменом.Затем вы можете создать простой командный объект, подобный этому:
class UndoLastStatusCommand {
public Guid OrderId { get; set; }
}
У OrderService был бы метод для обработки этой команды:
public void Process(UndoLastStatusCommand command) {
using (var unitOfWork = UowManager.Start()) {
var order = this.orderRepository.Get(command.OrderId);
if (order == null)
throw some exception
// operate on domain to undo last status
unitOfWork.Commit();
}
}
Итак, теперь модель домена для Order предоставляет все данные и поведение, соответствующие Заказу, но OrderService и уровень обслуживания в целом объявляют различные виды операций, которые выполняются над заказом, и предоставляют домен для использования внешними компонентами, такими как уровень представления.
Также рассмотрите возможность изучения концепции события домена в котором рассматриваются анемичные модели предметной области и пути их улучшения.
Похоже, вы не используете этот домен в тестах.Взгляните на работу Роб Венс, особенно его работы по исследовательскому моделированию, инверсии времени и активно-пассивному.