Шаблон проектирования «Спецификация» в C#

от автора

«Спецификация» в программировании — это шаблон проектирования, посредством которого представление правил бизнес логики может быть преобразовано в виде цепочки объектов, связанных операциями булевой логики.

Я познакомился с этим термином в процессе чтения DDD Эванса. На Хабре есть статьи с описанием практического применения паттерна и проблем, возникающих в процессе реализации.

Если коротко, основное преимущество от использования «спецификаций» в том, чтобы иметь одно понятное место, в котором сосредоточены все правила фильтрации объектов предметной модели, вместо тысячи размазанных ровным слоем по приложению лямбда-выражений.

Классическая реализация шаблона проектирования выглядит так:

public interface ISpecification {     bool IsSatisfiedBy(object candidate); } 

Что с ним не так применительно к C#?

  1. Есть Expression<Func<T, bool>> и Func<T, bool>>, сигнатура которых совпадает с IsSatisfiedBy
  2. Есть Extension-методы. alexanderzaytsev с помощью них делает вот так:
    public class UserQueryExtensions  {   public static IQueryable<User> WhereGroupNameIs(this IQueryable<User> users, strin name)   {       return users.Where(u => u.GroupName == name);   } } 

  3. А еще можно реализовать вот такую надстройку над LINQ:
    public abstract class Specification<T> {   public bool IsSatisfiedBy(T item)   {     return SatisfyingElementsFrom(new[] { item }.AsQueryable()).Any();   }     public abstract IQueryable<T> SatisfyingElementsFrom(IQueryable<T> candidates); } 

В конечном итоге возникает вопрос: стоит ли в C# пользоваться шаблоном десятилетней давности из мира Java и как его реализовать?


Мы решили, что стоит вот таким образом:

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 — отнюдь не тривиальная задача. Совсем не приятно, если требуется условная сборка выражения, например, если спецификация содержит три параметра, два из которых – не обязательные.

В конечном итоге, эти рассуждения навели на мысль, что самый простой способ – модифицировать целевой 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 и немного особой уличной магии 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); }  

ссылка на оригинал статьи https://habrahabr.ru/post/325280/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *