Шаблон спецификаций в дизайне, управляемом доменом

Я боролся с проблемой, связанной с DDD, с Спецификациями, и я много читал в DDD, спецификациях и репозиториях.

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

Сначала несколько очевидных фактов:

  • Репозитории для получения уровня DataAccess/Infrastructure Layer
  • Модели домена представляют собой бизнес-логику и переходят на уровень домена
  • Модели доступа к данным представляют собой уровень Persistence и переходят на уровень Persistance/Infrastructure/DataAccess.
  • Бизнес-логика переходит на уровень домена
  • Спецификации - это бизнес-логика, поэтому они также относятся к слою Domain.
  • Во всех этих примерах ORM Framework и SQL Server используются внутри репозитория
  • Модели Persistance могут не протекать в доменном слое

До сих пор так просто. Проблема возникает, когда/если мы пытаемся применить Спецификации к репозиторию и не нарушать шаблон DDD или проблемы с производительностью.

Возможные способы применения Спецификации:

1) Классический способ: спецификации с использованием модели домена в слое домена

Примените традиционный шаблон спецификации с помощью метода IsSatisfiedBy, возвращая bool и Composite Specifications для объединения нескольких спецификаций.

Это позволит нам сохранять спецификации в слое домена, но...

  • Он должен работать с моделями доменов, в то время как в репозитории используются модели Persistence, которые представляют структуру данных слоя persistence. Это можно легко исправить с помощью таких карт, как AutoMapper.
  • Однако проблема, которая не может быть решена: все спецификации должны быть выполнены в памяти. В большой таблице/базе данных это означает огромное влияние, если вам придется перебирать все объекты только для того, чтобы отфильтровать ту, которая соответствует вашим требованиям.

2) Технические характеристики с использованием модели устойчивости

Это похоже на 1), но с использованием моделей устойчивости в спецификации. Это позволяет напрямую использовать Спецификацию как часть нашего предиката .Where, который будет переведен в запрос (т.е. TSQL), и фильтрация будет выполняться в хранилище с сохранением (например, SQL Server).

  • Хотя это дает хорошие результаты, это явно нарушает шаблон DDD. Наша модель Persistence просачивается в слой Domain, что делает слой домена зависимым от уровня Persistence, а не наоборот.

3) Как 2), но сделайте Спецификации Часть слоя сдерживания

  • Это не работает, потому что для уровня домена требуется ссылка на Спецификации. Он все равно будет зависеть от уровня сохранения.
  • У нас была бы бизнес-логика внутри уровня Persistence. Что также нарушает шаблон DDD

4) Как 3, но используйте абстрактные спецификации как интерфейсы

У нас были бы интерфейсы спецификаций в нашем доменном слое, наши конкретные реализации спецификаций в слое Persistence. Теперь наш слой домена будет взаимодействовать только с интерфейсами и не будет зависеть от уровня Persistence.

  • Это все еще нарушает № 2 из 3). У нас была бы бизнес-логика в плане сохранения, что плохо.

5) Перевести дерево выражений из модели домена в модель сохранения.

Это, безусловно, решает проблему, но это нетривиальная задача, но она сохранит спецификации внутри нашего уровня домена, все еще получая пользу от оптимизации SQL, поскольку спецификации становятся частью предложения Repositories Where и переходят на TSQL

Я попытался использовать этот подход, и есть несколько проблем (сторона реализации формы):

  • Нам нужно знать конфигурацию из Mapper (если мы ее используем) или сохранить нашу собственную систему сопоставления. Это может быть частично выполнено (чтение конфигурации Mapper) с помощью AutoMapper, но существуют и другие проблемы.
  • Это приемлемо для одного, где одно свойство модели A сопоставляется с одним свойством модели B. Становится все труднее, если типы различны (т.е. из-за типов сохраняемости, например, перечисления, сохраняемые в виде строк или пар ключ/значение в другой таблице, и нам нужно сделать преобразования внутри распознавателя.
  • Это становится довольно сложным, если несколько полей отображаются в одно поле назначения. Я считаю, что это не проблема для моделей Domain Model → Persistence Model

** 6) Query Builder, как API **

Последний из них делает какой-то API запросов, который передается в спецификацию, и из которого слой репозитория/сохранения будет генерировать дерево выражений, которое будет передано в предложение .Where и которое использует интерфейс для объявления всех фильтруемых полей,

Я сделал несколько попыток в этом направлении, но не был слишком доволен результатами. Что-то вроде

public interface IQuery<T>
{
    IQuery<T> Where(Expression<Func<T, T>> predicate);
}
public interface IQueryFilter<TFilter>
{
    TFilter And(TFilter other);
    TFilter Or(TFilter other);
    TFilter Not(TFilter other);
}

public interface IQueryField<TSource, IQueryFilter>
{
    IQueryFilter Equal(TSource other);
    IQueryFilter GreaterThan(TSource other);
    IQueryFilter Greater(TSource other);
    IQueryFilter LesserThan(TSource other);
    IQueryFilter Lesser(TSource other);
}
public interface IPersonQueryFilter : IQueryFilter<IPersonQueryFilter>
{
    IQueryField<int, IPersonQueryFilter> ID { get; }
    IQueryField<string, IPersonQueryFilter> Name { get; }
    IQueryField<int, IPersonQueryFilter> Age { get; }
}

и в спецификации мы передадим IQuery<IPersonQueryFilter> query в конструктор спецификаций, а затем применим спецификации к нему при использовании или объединении.

IQuery<IGridQueryFilter> query = null;

query.Where(f => f.Name.Equal("Bob") );

Мне очень не нравится этот подход, так как он несколько затрудняет обработку сложных спецификаций (например, и/или цепочку), и мне не нравится, как работает And/Or/Not, особенно создавая деревья выражений из этого "API".

Я искал недели по всему Интернету, читал десятки статей по DDD и спецификациям, но они всегда обрабатывают простые случаи и не принимают во внимание эффективность или нарушают DDD шаблон.

Как вы решаете это в приложении реального мира, не занимаясь фильтрацией или утечкой памяти? Стойкость к уровню домена?

Существуют ли какие-либо фреймворки, которые решают вышеперечисленные проблемы одним из двух способов (Query Builder, например, синтаксисом для деревьев выражений или транслятором дерева выражений)?

Ответ 1

Я думаю, что шаблон спецификации не предназначен для критериев запроса. На самом деле, вся концепция DDD тоже не является. Рассмотрим CQRS, если существует множество требований к запросу.

Спецификация шаблона помогает развить вездесущий язык, я думаю, что он вроде как DSL. Он заявляет, что делать, а не как это делать. Например, в контексте заказа заказы считаются просроченными, если они были размещены, но не оплачены в течение 30 минут. С шаблоном Specification ваша команда может говорить с коротким, но уникальным термином: OverdueOrderSpecification. Представьте себе следующее:

case -1

Business people: I want to find out all overdue orders and ...  
Developer: I can do that, it is easy to find all satisfying orders with an overdue order specification and..

case -2

Business people: I want to find out all orders which were placed before 30 minutes and still unpaid...  
Developer: I can do that, it is easy to filter order from tbl_order where placed_at is less that 30minutes before sysdate....

Какой из них вы предпочитаете?

Обычно нам нужен обработчик DSL для синтаксического анализа dsl, в этом случае он может быть в адаптере persistence, переводит спецификацию в критерии запроса. Эта зависимость (infrastrructure.persistence = > domain) не нарушает принципала архитектуры.

class OrderMonitorApplication {
    public void alarm() {
       // The specification pattern keeps the overdue order ubiquitous language in domain
       List<Order> overdueOrders = orderRepository.findBy(new OverdueSpecification());
       for (Order order: overdueOrders) {
           //notify admin
       }
    }
}

class HibernateOrderRepository implements orderRepository {
    public List<Order> findBy(OrderSpecification spec) {
        criteria.le("whenPlaced", spec.placedBefore())//returns sysdate - 30
        criteria.eq("status", spec.status());//returns WAIT_PAYMENT
        return ...
    }
}

Ответ 2

Как только я выполнил спецификацию, но...

  • Он был основан на LINQ и IQueryable.
  • Он использовал единый унифицированный репозиторий (но для меня это неплохо, и я думаю, что это основная причина использования Спецификации).
  • В нем используется единая модель для домена и постоянных потребностей (что я считаю плохим).

Repository:

public interface IRepository<TEntity> where TEntity : Entity, IAggregateRoot
{
    TEntity Get<TKey>(TKey id);

    TEntity TryGet<TKey>(TKey id);

    void DeleteByKey<TKey>(TKey id);

    void Delete(TEntity entity);

    void Delete(IEnumerable<TEntity> entities);

    IEnumerable<TEntity> List(FilterSpecification<TEntity> specification);

    TEntity Single(FilterSpecification<TEntity> specification);        

    TEntity First(FilterSpecification<TEntity> specification);

    TResult Compute<TResult>(ComputationSpecification<TEntity, TResult> specification);

    IEnumerable<TEntity> ListAll();

    //and some other methods
}

Спецификация фильтра:

public abstract class FilterSpecification<TAggregateRoot> where TAggregateRoot : Entity, IAggregateRoot
{

     public abstract IQueryable<TAggregateRoot> Filter(IQueryable<TAggregateRoot> aggregateRoots);

     public static FilterSpecification<TAggregateRoot> CreateByPredicate(Expression<Func<TAggregateRoot, bool>> predicate)
     {
         return new PredicateFilterSpecification<TAggregateRoot>(predicate);
     }      

     public static FilterSpecification<TAggregateRoot> operator &(FilterSpecification<TAggregateRoot> op1, FilterSpecification<TAggregateRoot> op2)
     {
         return new CompositeFilterSpecification<TAggregateRoot>(op1, op2);
     }        

     public static FilterSpecification<TAggregateRoot> CreateDummy()
     {
         return new DummyFilterSpecification<TAggregateRoot>();
     }

}


public class CompositeFilterSpecification<TAggregateRoot> : FilterSpecification<TAggregateRoot> where TAggregateRoot : Entity, IAggregateRoot
{

    private readonly FilterSpecification<TAggregateRoot> _firstOperand;
    private readonly FilterSpecification<TAggregateRoot> _secondOperand;

    public CompositeFilterSpecification(FilterSpecification<TAggregateRoot> firstOperand, FilterSpecification<TAggregateRoot> secondOperand)
    {
        _firstOperand = firstOperand;
        _secondOperand = secondOperand;
    }

    public override IQueryable<TAggregateRoot> Filter(IQueryable<TAggregateRoot> aggregateRoots)
    {
        var operand1Results = _firstOperand.Filter(aggregateRoots);
        return _secondOperand.Filter(operand1Results);
    }
}

public class PredicateFilterSpecification<TAggregateRoot> : FilterSpecification<TAggregateRoot> where TAggregateRoot : Entity, IAggregateRoot
{

    private readonly Expression<Func<TAggregateRoot, bool>> _predicate;

    public PredicateFilterSpecification(Expression<Func<TAggregateRoot, bool>> predicate)
    {
        _predicate = predicate;
    }

    public override IQueryable<TAggregateRoot> Filter(IQueryable<TAggregateRoot> aggregateRoots)
    {
        return aggregateRoots.Where(_predicate);
    }
}

Другая спецификация:

public abstract class ComputationSpecification<TAggregateRoot, TResult> where TAggregateRoot : Entity, IAggregateRoot
{

    public abstract TResult Compute(IQueryable<TAggregateRoot> aggregateRoots);

    public static CompositeComputationSpecification<TAggregateRoot, TResult> operator &(FilterSpecification<TAggregateRoot> op1, ComputationSpecification<TAggregateRoot, TResult> op2)
    {
        return new CompositeComputationSpecification<TAggregateRoot, TResult>(op1, op2);
    }

}

и обычаи:

OrderRepository.Compute(new MaxInvoiceNumberComputationSpecification()) + 1
PlaceRepository.Single(FilterSpecification<Place>.CreateByPredicate(p => p.Name == placeName));
UnitRepository.Compute(new UnitsAreAvailableForPickingFilterSpecification() & new CheckStockContainsEnoughUnitsOfGivenProductComputatonSpecification(count, product));

Пользовательские реализации могут выглядеть как

public class CheckUnitsOfGivenProductExistOnPlaceComputationSpecification : ComputationSpecification<Unit, bool>
{
    private readonly Product _product;
    private readonly Place _place;

    public CheckUnitsOfGivenProductExistOnPlaceComputationSpecification(
        Place place,
        Product product)
    {
        _place = place;
        _product = product;
    }

    public override bool Compute(IQueryable<Unit> aggregateRoots)
    {
        return aggregateRoots.Any(unit => unit.Product == _product && unit.Place == _place);
    }
}

Наконец, я вынужден сказать, что простая реализация Specficiation плохо подходит в соответствии с DDD. Вы провели большие исследования в этой области, и вряд ли кто-то предложит что-то новое:). Кроме того, посмотрите http://www.sapiensworks.com/blog/ блог.

Ответ 3

Я опаздываю на вечеринку, ошибка вот мои 2 цента...

Я также пытался реализовать шаблон спецификации по тем же причинам, которые вы описали выше. Если вы откажетесь от требования к отдельной модели (постоянство/домен), тогда ваша проблема значительно упростится. Вы можете добавить другой метод в спецификацию, чтобы сгенерировать дерево выражений для ORM:

public interface ISpecification<T>
{
    bool IsSpecifiedBy(T item);
    Expression<Func<T, bool>> GetPredicate()
}

Есть сообщение от Вальдмира Хорикова, в котором подробно описывается, как это сделать.

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

В конце концов я пришел к решению, использующему посетителя для преобразования модели предметной области в дерево выражений модели постоянства.

Я недавно написал серию постов, где я объясняю

Конечный результат становится очень простым в использовании, вам нужно сделать спецификацию видимой...

public interface IProductSpecification
{
    bool IsSpecifiedBy(Product item);
    TResult Accept<TResult>(IProductSpecificationVisitor<TResult> visitor);
}

Создайте SpecificationVisitor для перевода спецификации в выражение:

public class ProductEFExpressionVisitor : IProductSpecificationVisitor<Expression<Func<EFProduct, bool>>> 
{
    public Expression<Func<EFProduct, bool>> Visit (ProductMatchesCategory spec) 
    {
        var categoryName = spec.Category.CategoryName;
        return ef => ef.Category == categoryName;
    }

    //other specification-specific visit methods
}

Есть только некоторые настройки, которые необходимо выполнить, если вы хотите создать общую спецификацию. Все это подробно описано в ссылках выше.

Ответ 4

Я искал недели по всему Интернету, читал десятки статьи о DDD и спецификации, но они всегда обрабатывают просто случаев и не учитывают эффективность, или они Нарушить шаблон DDD.

Кто-то исправит меня, если я ошибаюсь, но мне кажется, что концепция "модели сохранения" не появлялась до недавнего времени в пространстве DDD (кстати, где вы читали об этом?). Я не уверен, что это описано в оригинальной синей книге.

Я лично не вижу много преимуществ. Мое мнение состоит в том, что у вас есть постоянная реляционная модель (обычно) в вашей базе данных и модель домена в памяти в вашем приложении. Разрыв между ними перекрывается действием, а не моделью. Это действие может быть выполнено ORM. Мне еще предстоит продать тот факт, что "объектная модель Persistence" действительно имеет смысл семантически, не говоря уже об обязательном уважении принципов DDD (*).

Теперь существует подход CQRS, в котором у вас есть отдельная Read Model, но это совершенно другое животное, и я бы не видел Specifications, действующего на объекты Read Model вместо Entities, в качестве нарушения DDD в этом случае. Спецификация - это, в конце концов, очень общий шаблон, который ничто в DDD принципиально не ограничивает Entities.

(*) Редактировать: Создатель Automapper Джимми Богард, похоже, находит его слишком сложным - см. Как использовать autoapper для сопоставления отношений "многие ко многим" ?