- PVSM.RU - https://www.pvsm.ru -
«Спецификация» в программировании — это шаблон проектирования, посредством которого представление правил бизнес логики может быть преобразовано в виде цепочки объектов, связанных операциями булевой логики.
Я познакомился с этим термином в процессе чтения DDD Эванса [1]. На Хабре есть статьи с описанием практического применения паттерна [2] и проблем, возникающих в процессе реализации [3].
Если коротко, основное преимущество от использования «спецификаций» в том, чтобы иметь одно понятное место, в котором сосредоточены все правила фильтрации объектов предметной модели, вместо тысячи размазанных ровным слоем по приложению лямбда-выражений.
Классическая реализация шаблона проектирования выглядит так:
public interface ISpecification
{
bool IsSatisfiedBy(object candidate);
}
Expression<Func<T, bool>> и Func<T, bool>>, сигнатура которых совпадает с IsSatisfiedBypublic class UserQueryExtensions
{
public static IQueryable<User> WhereGroupNameIs(this IQueryable<User> users,
string name)
{
return users.Where(u => u.GroupName == name);
}
}
public abstract class Specification<T>
{
public bool IsSatisfiedBy(T item)
{
return SatisfyingElementsFrom(new[] { item }.AsQueryable()).Any();
}
public abstract IQueryable<T> SatisfyingElementsFrom(IQueryable<T> candidates);
}
Мы решили, что стоит вот таким образом:
public interface IQueryableSpecification<T>
where T: class
{
IQueryable<T> Apply(IQueryable<T> query);
}
public interface IQueryableOrderBy<T>
{
IOrderedQueryable<T> Apply(IQueryable<T> queryable);
}
public static bool Satisfy<T>(this T obj, Func<T, bool> spec) => spec(obj);
public static bool SatisfyExpresion<T>(this T obj, Expression<Func<T, bool>> spec)
=> spec.AsFunc()(obj);
public static bool IsSatisfiedBy<T>(this Func<T, bool> spec, T obj)
=> spec(obj);
public static bool IsSatisfiedBy<T>(this Expression<Func<T, bool>> spec, T obj)
=> spec.AsFunc()(obj);
public static IQueryable<T> Where<T>(this IQueryable<T> source,
IQueryableSpecification<T> spec)
where T : class
=> spec.Apply(source);
Func<T, bool>?
От Func очень сложно перейти к Expression. Чаще требуется перенести фильтрацию именно на уровень построения запроса к БД, иначе придется вытаскивать миллионы записей и фильтровать их в памяти, что не оптимально.
Expression<Func<T, bool>>?
Переход от Expression к Func, напротив, тривиален: var func = expression.Compile(). Однако, компоновка Expression — отнюдь не тривиальная задача [6]. Еще более не приятно, если требуется условная сборка выражения (например, если спецификация содержит три параметра, два из которых – не обязательные). А совсем плохо Expression<Func<T, bool>> справляется в случаях, требующих подзапросов вроде query.Where(x => someOtherQuery.Contains(x.Id)).
В конечном итоге, эти рассуждения навели на мысль, что самый простой способ – модифицировать целевой IQueryable и передавать далее через fluent interface. Дополнительные методы Where позволяют коду выглядеть, словно это обычная цепочка LINQ-преобразований.
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source,
IQueryableOrderBy<T> spec)
where T : class
=> spec.Apply(source);
public interface IQueryableOrderBy<T>
{
IOrderedQueryable<T> Apply(IQueryable<T> queryable);
}
Тогда, добавив Dynamic Linq [7] и немного особой уличной магии Reflection, можно написать базовый объект для фильтрации чего-угодно в декларативном стиле. Приведенный ниже код анализирует публичные свойства наследника AutoSpec и типа, к которому нужно применить фильтрацию. Если совпадение найдено и свойство наследника AutoSpec заполнено к Queryable автоматически будет добавлено правило фильтрации по данному полю.
public class AutoSpec<TProjection> : IPaging, ILinqSpecification<TProjection>, ILinqOrderBy<TProjection>
where TProjection : class, IHasId
{
public virtual IQueryable<TProjection> Apply(IQueryable<TProjection> query)
=> GetType()
.GetPublicProperties()
.Where(x => typeof(TProjection).GetPublicProperties()
.Any(y => x.Name == y.Name))
.Aggregate(query, (current, next) =>
{
var val = next.GetValue(this);
if (val == null) return current;
return current.Where(next.PropertyType == typeof(string)
? $"{next.Name}.StartsWith(@0)"
: $"{next.Name}=@0", val);
});
IOrderedQueryable<TProjection> ILinqOrderBy<TProjection>.Apply(
IQueryable<TProjection> queryable)
=> !string.IsNullOrEmpty(OrderBy)
? queryable.OrderBy(OrderBy)
: queryable.OrderBy(x => x.Id);
}
AutoSpecможно реализовать и безDynamic Linq, с помощью лишьExpression, но реализация не уместится в десять строчек и код будет гораздо сложнее для понимания.
Автор: Максим Аршинов
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/251384
Ссылки в тексте:
[1] DDD Эванса: https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215
[2] описанием практического применения паттерна: https://habrahabr.ru/post/171559/
[3] проблем, возникающих в процессе реализации: https://habrahabr.ru/post/260771/
[4] alexanderzaytsev: https://habrahabr.ru/users/alexanderzaytsev/
[5] вот такую надстройку над LINQ: https://nblumhardt.com/archives/implementing-the-specification-pattern-via-linq/
[6] отнюдь не тривиальная задача: https://habrahabr.ru/post/313394/
[7] Dynamic Linq: https://weblogs.asp.net/scottgu/dynamic-linq-part-1-using-the-linq-dynamic-query-library
[8] Источник: https://habrahabr.ru/post/325280/
Нажмите здесь для печати.