Подтвердить что ты не робот

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

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

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

  • Презентация
  • Сервис
  • Repository
  • Data/Entity

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

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

Любые мысли?

4b9b3361

Ответ 1

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

Услуги

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

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

Repository

Здесь ваш доступ к данным и согласованности идут. В чистом DDD ваши агрегированные корни будут ответственны за проверку согласованности (до сохранения любых объектов). В вашем случае вы будете использовать проверки на уровне Доменные службы.


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

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

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

Из вашего Репозитория используйте Спецификации на уровне Доменные службы, чтобы проверить целостность объекта и т.д., прежде чем продолжать свои объекты.

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

  • "Услуги и изолированный слой домена" (стр. 106)
  • "Технические характеристики" (стр. 224)
  • "Спецификации запросов" (стр. 229)

Ответ 2

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

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

Другими словами, Модель домена приходит сначала, и все реализации, специфичные для технологии, впоследствии адресуются 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) требуют, чтобы вы вывели классы данных из определенных базовых классов, поэтому это вообще не подходит для объектов домена как POCOs. Для этого требуется сопоставление.

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

Это может быть утомительной работой, но вы можете использовать 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);
}

Ответ 3

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

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

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

Ответ 4

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

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

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

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

Ответ 5

Если ваша технология 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);
   }

}

Ответ 7

Уровень обслуживания.

Ответ 8

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

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

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", но в противном случае я бы предложил обращаться с ними с уважением и сохранить все расширения сущностей в одном классе в его собственный файл.

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