Я боролся с проблемой, связанной с 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, например, синтаксисом для деревьев выражений или транслятором дерева выражений)?