Если вы вынуждены использовать модель домена Anemic, куда вы помещаете свою бизнес-логику и вычисляемые поля? - PullRequest
18 голосов
/ 19 декабря 2009

Наш текущий инструмент O / RM на самом деле не допускает модели с расширенными областями, поэтому мы вынуждены везде использовать анемичные (DTO) сущности. Это сработало нормально, но я продолжаю бороться с тем, куда поместить базовую объектную бизнес-логику и вычисляемые поля.

Текущие слои:

  • Презентация
  • Сервис
  • Репозиторий
  • Данные / Entity

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

Decimal CalculateTotal(LineItemEntity li)
{
  return li.Quantity * li.Price;
}

или

Decimal CalculateOrderTotal(OrderEntity order)
{
  Decimal orderTotal = 0;
  foreach (LineItemEntity li in order.LineItems)
  {
    orderTotal += CalculateTotal(li);
  }
  return orderTotal;
}

Есть мысли?

Ответы [ 8 ]

24 голосов
/ 22 декабря 2009

Вернемся к основам:

Услуга

Услуги бывают трех видов: Доменные службы , Службы приложений и Инфраструктурные услуги

  • Доменные службы : инкапсулирует бизнес-логику, которая не является естественной вписывается в доменный объект. В вашем случае все вашей бизнес-логики.
  • Службы приложений : используются внешними потребителями для связи с вашей системой
  • Инфраструктурные услуги : Используется для обозначения технических проблем (например, MSMQ, поставщик электронной почты и т. Д.)

Репозиторий

Именно здесь проходят проверки доступа к данным и . В чистом DDD ваши Aggregate Roots будут отвечать за проверку согласованности (перед сохранением каких-либо объектов). В вашем случае вы будете использовать чеки со своего уровня Domain Services .


Предлагаемое решение: Разделите существующие сервисы на части

Используйте новый слой Domain Services , чтобы инкапсулировать всю логику для ваших DTO и ваших проверок согласованности (используя Спецификации , может быть?).

Используйте Службу приложений , чтобы предоставить необходимые методы выборки (FetchOpenOrdersWithLines), которые перенаправляют запросы в Репозиторий (и используют дженерики, как предложил Джереми). Вы также можете использовать Спецификации запросов , чтобы обернуть свои запросы.

В вашем репозитории используйте Спецификации в вашем слое Domain Services для проверки целостности объектов и т. Д. Перед сохранением ваших объектов.

Вы можете найти дополнительную информацию в книге Эванса:

  • "Службы и уровень изолированного домена" (стр. 106)
  • «Технические характеристики» (стр. 224)
  • «Спецификации запроса» (стр. 229)
11 голосов
/ 25 декабря 2009

Мне хочется ответить Му , но я бы хотел уточнить. В итоге: Не позволяйте вашему выбору ORM диктовать, как вы определяете свою модель домена.

Цель модели предметной области состоит в том, чтобы быть богатым объектно-ориентированным API, который моделирует домен. Чтобы следовать истинному доменно-управляемому дизайну , модель домена должна быть определена без ограничений технологией .

Другими словами, Модель предметной области стоит на первом месте , и все специфичные для технологии реализации впоследствии рассматриваются mappers , которые отображают между Доменной Моделью и рассматриваемой технологией. Это часто включает оба способа: на уровень доступа к данным, где выбор ORM может вводить ограничения, и на уровень пользовательского интерфейса, где технология пользовательского интерфейса предъявляет дополнительные требования.

Если реализация чрезвычайно далека от модели предметной области, мы говорим о антикоррупционном уровне .

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

В качестве примера, давайте посмотрим на ваш объект заказа. Моделирование Ордена, не связанного с технологией, может привести нас к чему-то вроде этого:

public class Order
{
    // constructors and properties

    public decimal CalculateTotal()
    {
        return (from li in this.LineItems
                select li.CalculateTotal()).Sum();
    }
}

Обратите внимание, что это простой старый объект CLR ( POCO ) и, таким образом, он не ограничен технологией. Теперь вопрос в том, как вы получаете это из своего хранилища данных?

Это должно быть сделано через абстрактный IOrderRepository:

public interface IOrderRepository
{
    Order SelectSingle(int id);

    void Insert(Order order);

    void Update(Order order);

    void Delete(int id);

    // more, specialized methods can go here if need be
}

Теперь вы можете реализовать IOrderRepository, используя ваш ORM. Однако некоторые ORM (такие как Microsoft Entity Framework) требуют, чтобы вы извлекали классы данных из определенных базовых классов, так что это совсем не подходит для доменных объектов как POCO. Для этого требуется отображение.

Важно понимать, что у вас могут быть строго типизированные классы данных, которые семантически напоминают ваши доменные сущности. Однако это чистая деталь реализации, так что не смущайтесь. Класс Order, который происходит, например, от EntityObject не является классом домена - это деталь реализации, поэтому при реализации IOrderRepository необходимо сопоставить Order Data Class с Order Класс Домана .

Это может быть утомительная работа, но вы можете использовать AutoMapper , чтобы сделать это за вас.

Вот как может выглядеть реализация метода SelectSingle:

public Order SelectSinge(int id)
{
    var oe = (from o in this.objectContext.Orders
              where o.Id == id
              select o).First();
    return this.mapper.Map<OrderEntity, Order>(oe);
}
5 голосов
/ 19 декабря 2009

Это именно то, для чего предназначен сервисный уровень - я также видел приложения, где он называется уровнем BusinessLogic.

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

Уровень хранилища должен быть максимально обобщенным, поэтому он не подходит для бизнес-логики, которая индивидуальна для определенных классов.

4 голосов
/ 22 декабря 2009

Если ваша технология ORM хорошо обрабатывает только объекты DTO, это не значит, что вы должны выбрасывать объекты с расширенными объектами. Вы все еще можете обернуть свои объекты DTO объектами сущности:

public class MonkeyData
{
   public string Name { get; set; }
   public List<Food> FavoriteFood { get; set; }
}

public interface IMonkeyRepository
{
   Monkey GetMonkey(string name) // fetches DTO, uses entity constructor
   void SaveMonkey(Monkey monkey) // uses entity GetData(), stores DTO
}


public class Monkey
{
   private readonly MonkeyData monkeyData;

   public Monkey(MonkeyData monkeyData)
   {
      this.monkeyData = monkeyData;
   }

   public Name { get { return this.monkeyData.Name; } }

   public bool IsYummy(Food food)
   {
      return this.monkeyData.FavoriteFood.Contains(food);
   }

   public MonkeyData GetData()
   {
      // CLONE the DTO here to avoid giving write access to the
      // entity innards without business rule enforcement
      return CloneData(this.monkeyData);
   }

}
4 голосов
/ 22 декабря 2009

Из того, что вы говорите, может быть, вы слишком жестко думаете о своих слоях Service и Repository. Похоже, вы не хотите, чтобы ваш уровень презентации имел прямую зависимость от уровня репозитория, и для достижения этого вы дублируете методы из своих репозиториев (ваших транзитных методов) в слое Service.

Я бы усомнился в этом. Вы можете расслабиться и позволить использовать оба в вашем слое Presentation и сделать вашу жизнь проще для начала. Возможно, спросите себя, чего вы добиваетесь, скрывая хранилища подобного рода. Вы уже абстрагируете настойчивость и запрашиваете РЕАЛИЗАЦИЮ вместе с ними. Это здорово и для чего они предназначены. Кажется, что вы пытаетесь создать сервисный слой, который скрывает тот факт, что ваши сущности вообще сохраняются. Я спросил бы почему?

Что касается расчета итоговых сумм заказа и т. Д. Ваш уровень обслуживания будет естественным домом. Класс SalesOrderCalculator с методами LineTotal (LineItem lineItem) и OrderTotal (Order order) вполне подойдет. Вы также можете рассмотреть возможность создания соответствующей фабрики, например, OrderServices.CreateOrderCalculator () для переключения реализации, если требуется (например, налог на скидку на заказ имеет специфические для страны правила). Это также может сформировать единую точку входа для заказа услуг и упростить поиск вещей с помощью IntelliSense.

Если все это звучит неосуществимо, возможно, вам следует подумать о том, чего достигают ваши абстракции, как они связаны друг с другом и Принцип единой ответственности . Репозиторий - это абстракция инфраструктуры (скрытие того, КАК объекты сохраняются и извлекаются). Сервисы абстрагируются от реализации бизнес-действий или правил и обеспечивают лучшую структуру управления версиями или отклонениями. Они, как правило, не наслоены так, как вы описываете. Если у вас есть сложные правила безопасности в ваших Сервисах, ваши репозитории могут быть лучше дома. В типичной модели стиля DDD репозитории, сущности, объекты-значения и сервисы будут использоваться рядом друг с другом на одном уровне и как часть одной модели. Поэтому слои выше (обычно представление) будут изолированы этими абстракциями. В рамках модели реализация одного сервиса может использовать абстракцию другого. Дальнейшее уточнение добавляет правила для тех, кто содержит ссылки на какие сущности или объекты-значения, обеспечивающие более формальный контекст жизненного цикла. Для получения дополнительной информации я бы порекомендовал изучить книгу Эрика Эванса или Дизайн, управляемый доменом, быстро .

3 голосов
/ 24 декабря 2009

Я обнаружил, что новая книга Дино Эспозито Microsoft® .NET: Разработка приложений для предприятия является отличным хранилищем знаний для этих типов вопросов и проблем.

1 голос
/ 29 декабря 2009

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

Например (из ваших примеров):

public static class LineItemEntityExtensions
{
  public static decimal CalculateTotal(this LineItemEntity li)
  {
    return li.Quantity * li.Price;
  }
}

public static class OrderEntityExtensions
{
  public static decimal CalculateOrderTotal(this OrderEntity order)
  {
    decimal orderTotal = 0;
    foreach (LineItemEntity li in order.LineItems)
      orderTotal += li.CalculateTotal();
    return orderTotal;
  }
}

public class SomewhereElse
{
  public void DoSomething(OrderEntity order)
  {
    decimal total = order.CalculateOrderTotal();
    ...
  }
}

Если таких добавлений очень мало, вы можете просто поместить их все в класс «DomainExtensions», но в противном случае я бы предложил относиться к ним с регулярным уважением и сохранять все расширения сущности в одном классе. свой собственный файл.

К вашему сведению: единственный раз, когда я это делал, это когда у меня было решение L2S и я не хотел связываться с частями. У меня также не было много расширений, потому что решение было маленьким. Мне больше нравится идея перехода на полноценный слой доменных служб.

1 голос
/ 19 декабря 2009

Сервисный уровень.

...