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

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

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

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

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

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

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


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

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


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


Мы решили, что стоит вот таким образом:
public interface IQueryableSpecification
    where T: class 
{
    IQueryable Apply(IQueryable query);
}

public interface IQueryableOrderBy
{
    IOrderedQueryable Apply(IQueryable queryable);
}

public static bool Satisfy(this T obj, Func spec) => spec(obj);

public static bool SatisfyExpresion(this T obj, Expression> spec)
=> spec.AsFunc()(obj);

public static bool IsSatisfiedBy(this Func spec, T obj)
=> spec(obj);

public static bool IsSatisfiedBy(this Expression> spec, T obj) 
=> spec.AsFunc()(obj);

public static IQueryable Where(this IQueryable source, 
IQueryableSpecification spec)
    where T : class
    => spec.Apply(source);

Почему не Func?


От Func очень сложно перейти к Expression. Чаще требуется перенести фильтрацию именно на уровень построения запроса к БД, иначе придется вытаскивать миллионы записей и фильтровать их в памяти, что не оптимально.

Почему не Expression>?


Переход от Expression к Func, напротив, тривиален: var func = expression.Compile(). Однако, компоновка Expression — отнюдь не тривиальная задача. Еще более не приятно, если требуется условная сборка выражения (например, если спецификация содержит три параметра, два из которых — не обязательные). А совсем плохо Expression> справляется в случаях, требующих подзапросов вроде query.Where(x => someOtherQuery.Contains(x.Id)).

В конечном итоге, эти рассуждения навели на мысль, что самый простой способ — модифицировать целевой IQueryable и передавать далее через fluent interface. Дополнительные методы Where позволяют коду выглядеть, словно это обычная цепочка LINQ-преобразований.

Руководствуясь этой логикой, можно выделить абстракцию для сортировки


public static IOrderedQueryable OrderBy(this IQueryable source, 
IQueryableOrderBy spec)
    where T : class
    => spec.Apply(source);

public interface IQueryableOrderBy
{
    IOrderedQueryable Apply(IQueryable queryable);
}

Тогда, добавив Dynamic Linq и немного особой уличной магии Reflection, можно написать базовый объект для фильтрации чего-угодно в декларативном стиле. Приведенный ниже код анализирует публичные свойства наследника AutoSpec и типа, к которому нужно применить фильтрацию. Если совпадение найдено и свойство наследника AutoSpec заполнено к Queryable автоматически будет добавлено правило фильтрации по данному полю.
public class AutoSpec : IPaging, ILinqSpecification, ILinqOrderBy
    where TProjection : class, IHasId
{
    public virtual IQueryable Apply(IQueryable 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 ILinqOrderBy.Apply(
        IQueryable queryable)
        => !string.IsNullOrEmpty(OrderBy)
        ? queryable.OrderBy(OrderBy)
        : queryable.OrderBy(x => x.Id);
}

AutoSpec можно реализовать и без Dynamic Linq, с помощью лишь Expression, но реализация не уместится в десять строчек и код будет гораздо сложнее для понимания.

Комментарии (0)

© Habrahabr.ru