Недооцененный паттерн «Спецификация» в связке с паттерном «Репозиторий»

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Использование спецификации открыло для меня новый мир в создании приложений

Мотивация

Репозитории предоставляют удобное решение для доступа к данным. Однако за многолетний опыт разработки, побывав в нескольких компаниях, сменив кучу проектов я НЕ ВСТРЕЧАЛ паттерн "Спецификация" совместно с паттерном "Репозиторий".

Плюсы:

  • использование абстракций для доступа к данным – правильное решение;

  • спецификация предлагает стандартизованный подход к созданию репозиториев, что облегчает разработку, сопровождение и масштабирование приложений;

  • добавление разных вариаций запросов данных сводится к созданию одной  строки кода;

  • изменение запросов производится на уровне пользовательского кода — нет необходимости менять код репозитория;

  • паттерн спецификация позволяет более гибко работать с фильтрами и операциями And, Or, Not и т. д.

Минусы

Уверен, ребята в комментариях найдут минусы у этого подхода.
Хотя от себя добавлю, что для оптимизированных запросов к БД, код нужно дорабатывать (хотя бы в том плане, чтобы можно было управлять порядком включения зависимых сущностей .Include(_ => _.AnotherEntity))


Реализация спецификации

Схема моей реализации шаблона представлена на Рисунке 1 ниже. Для моих потребностей она оказалась вполне достаточной и покрывающий все кейсы использования. Хочу обратить внимание, что у интерфейса нет методов And() Or() Not(). Благодаря этому нет нарушения Interface Segregation Principle.

Рис 1. UML схема шаблона "Спецификация"
Рис 1. UML схема шаблона "Спецификация"

Всего 4 класса и пара вспомогательных позволяют достичь ОГРОМНОЙ гибкости в формировании запросов.

Для корректного преобразования наших будущих условий в деревья выражений, с которыми работает любой фреймворк доступа к данным, использую Expression<Func>.

Код интерфейса ISpecification<TEntity>
/// <summary>
///     Базовый интерфейс спецификации
/// </summary>
public interface ISpecification<TEntity> : ICloneable
    where TEntity : class
{
    /// <summary>
    ///     Сколько объектов пропустить
    /// </summary>
    int Skip { get; }

    /// <summary>
    ///     Сколько объектов взять
    /// </summary>
    int Take { get; }

    /// <summary>
    ///     Получить необходимые поля для включения
    /// </summary>
    Expression<Func<TEntity, object>>[] GetIncludes();

    /// <summary>
    ///     Удовлетворяет ли объект условиям
    /// </summary>
    Expression<Func<TEntity, bool>>? SatisfiedBy();

    /// <summary>
    ///     Получить модели для сортировки результатов
    /// </summary>
    OrderModel<TEntity>[] GetOrderModels();
}

Метод Expression<Func<TEntity, object>>[] GetIncludes()позволяет возвращать функции включения объектов в запрос.

Метод Expression<Func<TEntity, bool>>? SatisfiedBy() занимается проверкой объекта на соответствие условиям перечисленным в Func<TEntity, bool>.

Метод OrderModel[] GetOrderModels() возвращает DTO, хранящие сортирующие выражения, для сортировки результатов запроса.

Код класса OrderModel<TEntity>
/// <summary>
///     Модель для хранения сортирующего выражения
/// </summary>
public class OrderModel<TEntity>
    where TEntity : class
{
    #region .ctor

    /// <inheritdoc cref="OrderModel{TEntity}" />
    public OrderModel(Expression<Func<TEntity, object>> orderExpression, bool needOrderByDescending)
    {
        OrderExpression = orderExpression;
        NeedOrderByDescending = needOrderByDescending;
    }

    #endregion

    #region Properties

    /// <summary>
    ///     Сортирующее выражение
    /// </summary>
    public Expression<Func<TEntity, object>> OrderExpression { get; }

    /// <summary>
    ///     Нужна ли сортировка по убыванию
    /// </summary>
    public bool NeedOrderByDescending { get; }

    #endregion
}

Абстрактный класс BaseSpecification<TEntity> содержит реализацию свойств Skip и Take , а также перегрузки операторов И (&) и ИЛИ (|). Благодаря чему нет необходимости внедрять методы And() и Or() в базовый интерфейс.

Код класса BaseSpecification<TEntity>
/// <summary>
///     Базовая спецификация для коллекций объектов
/// </summary>
public abstract class SpecificationBase<TEntity> : ISpecification<TEntity>
    where TEntity : class
{
    #region Implementation of ISpecification

    /// <inheritdoc />
    public int Skip { get; set; } = 0;

    /// <inheritdoc />
    public int Take { get; set; } = int.MaxValue;

    /// <inheritdoc />
    public abstract Expression<Func<TEntity, bool>>? SatisfiedBy();

    /// <inheritdoc />
    public abstract Expression<Func<TEntity, object>>[] GetIncludes();

    /// <inheritdoc />
    public abstract OrderModel<TEntity>[] GetOrderModels();

    /// <inheritdoc />
    public abstract object Clone();

    #endregion

    /// <summary>
    ///     Перегрузка оператора И
    /// </summary>
    public static SpecificationBase<TEntity> operator &(
        SpecificationBase<TEntity> left,
        SpecificationBase<TEntity> right)
    {
        return new AndSpecification<TEntity>(left, right);
    }

    /// <summary>
    ///     Перегрузка оператора ИЛИ
    /// </summary>
    public static SpecificationBase<TEntity> operator |(
        SpecificationBase<TEntity> left,
        SpecificationBase<TEntity> right)
    {
        return new OrSpecification<TEntity>(left, right);
    }
}

Самой простой в реализации является DirectSpecification<TEntity>. Она позволяет создавать одно условное выражение для выбора данных.

Код класса DirectSpecification<TEntity>
/// <summary>
///     Прямая спецификация
/// </summary>
public class DirectSpecification<TEntity> : SpecificationBase<TEntity>
    where TEntity : class
{
    #region Fields

    private readonly List<Expression<Func<TEntity, object>>> _includes = new();
    private readonly Expression<Func<TEntity, bool>>? _matchingCriteria;
    private OrderModel<TEntity>? _orderModel;

    #endregion

    #region .ctor

    /// <inheritdoc cref="DirectSpecification{TEntity}" />
    public DirectSpecification(Expression<Func<TEntity, bool>> matchingCriteria)
    {
        _matchingCriteria = matchingCriteria;
    }

    /// <inheritdoc cref="DirectSpecification{TEntity}" />
    public DirectSpecification()
    { }

    /// <inheritdoc cref="DirectSpecification{TEntity}" />
    protected DirectSpecification(
        List<Expression<Func<TEntity, object>>> includes,
        Expression<Func<TEntity, bool>>? matchingCriteria,
        OrderModel<TEntity>? orderModel)
    {
        _includes = includes;
        _matchingCriteria = matchingCriteria;
        _orderModel = orderModel;
    }

    #endregion

    #region Implementation of SpecificationBase

    /// <inheritdoc />
    public override object Clone()
    {
        // NOTE: поскольку список не смотрит из объекта явно,
        // то нет необходимости перекопировать его полностью включая внутренние элементы
        // аналогично и с моделью сортировки, считается, что она неизменяемая
        return new DirectSpecification<TEntity>(_includes, _matchingCriteria, _orderModel);
    }

    /// <inheritdoc />
    public override Expression<Func<TEntity, bool>>? SatisfiedBy()
        => _matchingCriteria;

    /// <inheritdoc />
    public override Expression<Func<TEntity, object>>[] GetIncludes()
        => _includes.ToArray();

    /// <inheritdoc />
    public override OrderModel<TEntity>[] GetOrderModels()
    {
        return _orderModel is null ? Array.Empty<OrderModel<TEntity>>() : new[] { _orderModel };
    }

    #endregion

    #region Public methods

    /// <summary>
    ///     Добавить включение
    /// </summary>
    public DirectSpecification<TEntity> AddInclude(Expression<Func<TEntity, object>> includeExpression)
    {
        _includes.Add(includeExpression);

        return this;
    }

    /// <summary>
    ///     Установить модель сортировки
    /// </summary>
    public DirectSpecification<TEntity> SetOrder(OrderModel<TEntity> orderModel)
    {
        _orderModel = orderModel;

        return this;
    }

    #endregion
}

"И" и "ИЛИ" спецификации между собой очень похожи, их код приведен ниже. Их конструкторы принимают в аргументах две другие спецификации ISpecification<TEntity>, которые могут быть как составными (тоже "И" или "ИЛИ"), так и простые спецификации (например две реализации через DirectSpecification<TEntity>), так и комбинации простой и составной спецификации.

Код класса AndSpecification<TEntity>
/// <summary>
///     Спецификация И
/// </summary>
public sealed class AndSpecification<TEntity> : SpecificationBase<TEntity>
   where TEntity : class
{
    #region Fields

    private readonly ISpecification<TEntity> _rightSideSpecification;
    private readonly ISpecification<TEntity> _leftSideSpecification;

    #endregion

    #region .ctor

    /// <inheritdoc />
    public override object Clone()
    {
        var left = (ISpecification<TEntity>)_leftSideSpecification.Clone();
        var right = (ISpecification<TEntity>)_leftSideSpecification.Clone();

        return new AndSpecification<TEntity>(left, right);
    }

    /// <inheritdoc cref="AndSpecification{TEnity}" />
    public AndSpecification(
        ISpecification<TEntity> leftSide,
        ISpecification<TEntity> rightSide)
    {
        Assert.NotNull(leftSide, "Left specification cannot be null");
        Assert.NotNull(rightSide, "Right specification cannot be null");

        _leftSideSpecification = leftSide;
        _rightSideSpecification = rightSide;
    }

    #endregion

    #region Implementation Of SpecificationBase

    /// <inheritdoc />
    public override Expression<Func<TEntity, bool>>? SatisfiedBy()
    {
        var left = _leftSideSpecification.SatisfiedBy();
        var right = _rightSideSpecification.SatisfiedBy();
        if (left is null && right is null)
        {
            return null;
        }

        if (left is not null && right is not null)
        {
            return left.And(right);
        }

#pragma warning disable IDE0046 // Convert to conditional expression
        if (left is not null)
        {
            return left;
        }
#pragma warning restore IDE0046 // Convert to conditional expression

        return right;
    }

    /// <inheritdoc />
    public override Expression<Func<TEntity, object>>[] GetIncludes()
    {
        var leftIncludes = _leftSideSpecification.GetIncludes();
        var rightIncludes = _rightSideSpecification.GetIncludes();

        leftIncludes.AddRange(rightIncludes);

        return leftIncludes;
    }

    /// <inheritdoc />
    public override OrderModel<TEntity>[] GetOrderModels()
    {
        var leftOrderModels = _leftSideSpecification.GetOrderModels();
        leftOrderModels.AddRange(_rightSideSpecification.GetOrderModels());

        return leftOrderModels;
    }

    #endregion
}

Код класса OrSpecification<TEntity>
/// <summary>
///     Спецификация ИЛИ
/// </summary>
public class OrSpecification<TEntity> : SpecificationBase<TEntity>
    where TEntity : class
{
    #region Fields

    private readonly ISpecification<TEntity> _leftSideSpecification;
    private readonly ISpecification<TEntity> _rightSideSpecification;

    #endregion

    #region .ctor

    /// <inheritdoc cref="OrSpecification{TEnity}" />
    public OrSpecification(
        ISpecification<TEntity> left,
        ISpecification<TEntity> right)
    {
        Assert.NotNull(left, "Left specification cannot be null");
        Assert.NotNull(right, "Right specification cannot be null");

        _leftSideSpecification = left;
        _rightSideSpecification = right;
    }

    #endregion

    #region Implemtation of SpecificationBase

    /// <inheritdoc />
    public override object Clone()
    {
        var left = (ISpecification<TEntity>)_leftSideSpecification.Clone();
        var  right = (ISpecification<TEntity>)_leftSideSpecification.Clone();

        return new OrSpecification<TEntity>(left, right);
    }

    /// <inheritdoc />
    public override Expression<Func<TEntity, bool>>? SatisfiedBy()
    {
        var left = _leftSideSpecification.SatisfiedBy();
        var right = _rightSideSpecification.SatisfiedBy();
        if (left is null && right is null)
        {
            return null;
        }

        if (left is not null && right is not null)
        {
            return left.Or(right);
        }

#pragma warning disable IDE0046 // Convert to conditional expression
        if (left is not null)
        {
            return left;
        }

        return right;
    }

    /// <inheritdoc />
    public override Expression<Func<TEntity, object>>[] GetIncludes()
    {
        var leftIncludes = _leftSideSpecification.GetIncludes();
        var rightIncludes = _rightSideSpecification.GetIncludes();

        leftIncludes.AddRange(rightIncludes);

        return leftIncludes;
    }

    /// <inheritdoc />
    public override OrderModel<TEntity>[] GetOrderModels()
    {
        var leftOrderModels = _leftSideSpecification.GetOrderModels();
        leftOrderModels.AddRange(_rightSideSpecification.GetOrderModels());

        return leftOrderModels;
    }

    #endregion
}

Обе они реализуют метод SatisfiedBy() базового класса SpecificationBase<TEntity>, объединяя два Expression<Func>, полученных от вызовов методов двух спецификаций, которые были переданы в конструктор.


Реализация репозиториев

Схема моей реализации репозиториев совместно с использованием паттерна "Спецификация" представлена на Рисунке 2 ниже.

Рис 2. UML схема паттерна "Репозиторий" совместно со "Спецификацией"
Рис 2. UML схема паттерна "Репозиторий" совместно со "Спецификацией"

В целом первые 4 метода (Get, GetStrict, List, Any), представленные в IRepository<TEntity>, реализуются единожды в базовом абстрактном классе StorageBase<TEntity> и больше никогда не изменятся.

Код интерфейса IStorage<TEntity>
/// <summary>
///     Общий интерфейс хранилищ
/// </summary>
public interface IStorage<T>
    where T : class
{
    /// <summary>
    ///     Добавляет новую модель в хранилище
    /// </summary>
    void Add(T model);
    
    /// <summary>
    ///     Удалить
    /// </summary>
    void Remove(T model);

    /// <summary>
    ///     Находит модель по идентификатору
    /// </summary>
    /// <param name="specification"> Спецификация получения данных </param>
    /// <returns> Модель </returns>
    T? Get(ISpecification<T> specification);

    /// <summary>
    ///     Находит модель по идентификатору, бросает ошибку, если не найдено
    /// </summary>
    /// <param name="specification"> Спецификация получения данных </param>
    /// <param name="errorCode"> Код ошибки, если модель не найдена </param>
    /// <returns> Модель </returns>
    /// <exception cref="ErrorException">
    ///     Ошибка с кодом <paramref name="errorCode" />, если модель не найдена
    /// </exception>
    T GetStrict(ISpecification<T> specification, string errorCode);

    /// <summary>
    ///     Определяет соответствуют ли выбранные объекты условиям спецификации
    /// </summary>
    /// <param name="specification"> Спецификация </param>
    bool Any(ISpecification<T> specification);

    /// <summary>
    ///     Получить сущности
    /// </summary>
    /// <param name="specification"> Спецификация </param>
    IEnumerable<T> GetMany(ISpecification<T> specification);
}

Ниже приведены реализации методов (Get, GetStrict, List, Any), они максимально просты и понятны, но при этом максимально "гибкие", благодаря спецификациям.

    /// <inheritdoc />
    public bool Any(ISpecification<T> specification)
        => SpecificationEvaluator
              .GetQuery(CreateQuery(), specification)
              .Any();

    /// <inheritdoc />
    public T? Get(ISpecification<T> specification)
        => SpecificationEvaluator
              .GetQuery(CreateQuery(), specification)
              .FirstOrDefault();

    /// <inheritdoc />
    public T GetStrict(ISpecification<T> specification, string errorCode)
        => SpecificationEvaluator
              .GetQuery(CreateQuery(), specification)
              .FirstOrDefault() ?? throw new ErrorException(errorCode);

    /// <inheritdoc />
    public IEnumerable<T> GetMany(ISpecification<T> specification)
        => SpecificationEvaluator
              .GetQuery(CreateQuery(), specification);

А теперь представляю гвоздь, опору, связующее звено без которого ничего не могло бы работать вместе.

Внимательные читатели задались вопросом про странный класс, появившийся непонятно откуда

Да) это класс SpecificationEvaluator

Этот класс позволяет формировать запросы на основе переданной спецификации к базе данных, хранилищам в RAM или другим источникам данных. Реализация для IEnumerableполностью аналогична, ее не буду приводить, для облегчения восприятия.

/// <summary>
///     Создает запрос к базе данных на основе спецификации
/// </summary>
public class SpecificationEvaluator
{
    #region IQueryable

    /// <summary>
    ///     Получить сформированный запрос
    /// </summary>
    public static IQueryable<TEntity> GetQuery<TEntity>(
        IQueryable<TEntity> inputQuery,
        ISpecification<TEntity> specification)
        where TEntity : class
    {
        var query = inputQuery;

        // включаю в запрос необходимые дополнительные сущности
        query = specification
            .GetIncludes()
            .Aggregate(query, static (current, include) => current.Include(include));

        // отбираю только необходимые объекты
        var whereExp = specification.SatisfiedBy();
        if (whereExp is not null)
        {
            query = query.Where(whereExp)!;
        }

        // получаю модели для сортировки
        var orderModels = specification.GetOrderModels();
        if (!orderModels.Any())
        {
            return query
                .Skip(specification.Skip)
                .Take(specification.Take);
        }

        // сортирую
        var orderedQuery = AddFirstOrderExpression(query, orderModels.First());
        foreach (var orderModel in orderModels.Skip(1))
        {
            orderedQuery = AddAnotherOrderExpression(orderedQuery, orderModel);
        }

        return orderedQuery
            .Skip(specification.Skip)
            .Take(specification.Take);
    }

    /// <summary>
    ///     Добавить сортировку в самый первый раз
    /// </summary>
    private static IOrderedQueryable<TEntity> AddFirstOrderExpression<TEntity>(
        IQueryable<TEntity> query,
        OrderModel<TEntity> orderModel)
        where TEntity : class
    {
        return orderModel.NeedOrderByDescending
            ? query.OrderByDescending(orderModel.OrderExpression)
            : query.OrderBy(orderModel.OrderExpression);
    }

    /// <summary>
    ///     Продолжить добавление сортировок
    /// </summary>
    private static IOrderedQueryable<TEntity> AddAnotherOrderExpression<TEntity>(
        IOrderedQueryable<TEntity> query,
        OrderModel<TEntity> orderModel)
        where TEntity : class
    {
        return orderModel.NeedOrderByDescending
            ? query.ThenByDescending(orderModel.OrderExpression)
            : query.ThenBy(orderModel.OrderExpression);
    }

    #endregion
}

Применение

Для упрощения понимания использования, приведу конкретный пример. Представим, что у нас есть класс Man - человек со свойствами имя, возраст и пол:

/// <summary>
///     Человек
/// </summary>
internal sealed class Man
{
    /// <summary>
    ///     Возраст
    /// </summary>
    public int Age { get; set; }

    /// <summary>
    ///     Имя
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    ///     Пол
    /// </summary>
    public GenderType Gender { get; set; }
}

/// <summary>
///     Определяет пол человека
/// </summary>
internal enum GenderType
{
    /// <summary>
    ///     Мужчина
    /// </summary>
    Male,

    /// <summary>
    ///     Женщина
    /// </summary>
    Female
}

Предположим, на нашем сайте мы реализуем множественный фильтр по каждому из полей этой сущности. Различные варианты самых распространенных репозиториев, которые я считаю сложными, неподдерживаемыми.

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

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

Буду показывать на примере RAM хранилища данных для более легкого воспроизведения.

/// <summary>
///     Самое неоптимальное хранилище моделей людей
/// </summary>
internal sealed class BadManRepository1 : StorageBase<Man>
{
    private ImmutableArray<Man> _storage = ImmutableArray<Man>.Empty;

    /// <inheritdoc />
    public override void Add(Man model)
    {
        _storage = _storage.Add(model);
    }

    /// <inheritdoc />
    public override void Remove(Man model)
    {
        _storage = _storage.Remove(model);
    }

    /// <inheritdoc />
    public Man Get(string name)
        => CreateQuery().FirstOrDefault(_ => _.Name == name);

    /// <inheritdoc />
    public Man Get(int age)
        => CreateQuery().FirstOrDefault(_ => _.Age == age);

    /// <inheritdoc />
    public Man Get(GenderType gender)
        => CreateQuery().FirstOrDefault(_ => _.Gender == gender);

    /// <inheritdoc />
    public Man Get(string name, int age)
        => CreateQuery().FirstOrDefault(_ => _.Name == name && _.Age == age);

    /// <inheritdoc />
    public Man Get(string name, GenderType gender)
        => CreateQuery().FirstOrDefault(_ => _.Name == name && _.Gender == gender);

    /// <inheritdoc />
    public Man Get(string name, int age, GenderType gender)
        => CreateQuery().FirstOrDefault(_ => _.Name == name && _.Age == age && _.Gender == gender);

    /// <inheritdoc />
    public Man Get(string name, int age)
        => CreateQuery().FirstOrDefault(_ => _.Name == name || _.Age == age);
}

Вариант выше содержит много метода Get, но даже они не покрывают всех возможных вариантов фильтрации для трех полей, которых с учетом операторов И и ИЛИ уже я насчитал 11, а что будет для большего количества полей, а если еще нужны методы Any() , List() с такими же условиями фильтрации?

Другой подход уменьшает количество методов в репозитории, но увеличивает количество строк кода в каждом из них. Он тоже не является оптимальным. Привел реализацию только метода Get с И оператором. Нужно также реализовать Get с ИЛИ и Get с вариациями И и ИЛИ. Все это займет кучу кода и при добавлении нового свойства в класс Man, придется изменять каждый из этих методов или добавлять новые.

/// <summary>
///     Неоптимальное хранилище моделей людей
/// </summary>
internal sealed class BadManRepository2 : StorageBase<Man>
{
    private ImmutableArray<Man> _storage = ImmutableArray<Man>.Empty;

    /// <inheritdoc />
    public Man Get(GetManRequest request)
    {
        var query = CreateQuery();
        if (request.Name is not null)
        {
            query = query.Where(x => x.Name == request.Name);
        }
        if (request.Age is not null)
        {
            query = query.Where(x => x.Age == request.Age);
        }
        if (request.Gender is not null)
        {
            query = query.Where(x => x.Gender == request.Gender);
        }

        return query.FirstOrDefault();
    }
}

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

/// <summary>
///     Хранилище моделей людей
/// </summary>
internal sealed class ManRepository : StorageBase<Man>
{
    private ImmutableArray<Man> _storage = ImmutableArray<Man>.Empty;

    /// <inheritdoc />
    public override void Add(Man model)
    {
        _storage = _storage.Add(model);
    }

    /// <inheritdoc />
    public override void Remove(Man model)
    {
        _storage = _storage.Remove(model);
    }

    /// <inheritdoc />
    protected override IEnumerable<Man> CreateQuery() => _storage;
}

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

/// <summary>
///     Статический класс для создания спецификации для получения <see cref="Man" />
/// </summary>
internal static class ManSpecification
{
    /// <summary>
    ///     С именем
    /// </summary>
    public static ISpecification<Man> WithName(string name)
        => new DirectSpecification<Man>(_ => _.Name == name);

    /// <summary>
    ///     С возрастом
    /// </summary>
    public static ISpecification<Man> WithAge(int age)
        => new DirectSpecification<Man>(_ => _.Age == age);

    /// <summary>
    ///     С гендером
    /// </summary>
    public static ISpecification<Man> WithGender(GenderType gender)
        => new DirectSpecification<Man>(_ => _.Gender == gender);

    /// <summary>
    ///     Сортировать по возрасту
    /// </summary>
    public static ISpecification<Man> OrderByAge(bool orderByDescending = false)
        => new DirectSpecification<Man>().SetOrder(new(static _ => _.Age, orderByDescending));

    /// <summary>
    ///     Сортировать по имени
    /// </summary>
    public static ISpecification<Man> OrderByName(bool orderByDescending = false)
        => new DirectSpecification<Man>().SetOrder(new(static _ => _.Name, orderByDescending));
}

Тогда применение будет выглядеть следующим образом:

    public static int Main()
    {
        var repository = new ManRepository();

        var spec1 = ManSpecification.WithName("Коля")
            & ManSpecification.WithAge(26);

        var man1 = repository.Get(spec1);

        var spec2 = ManSpecification.WithName("Коля") | ManSpecification.WithAge(26);
        var men2 = repository.Get(spec2);

        var spec3 = (ManSpecification.WithName("Женя") | ManSpecification.WithAge(26))
            & ManSpecification.WithGender(GenderType.Male);
        var men3 = repository.Get(spec2);

        var spec4 = (ManSpecification.WithName("Женя") | ManSpecification.WithAge(26))
            & ManSpecification.WithGender(GenderType.Male)
            & ManSpecification.OrderByAge();
        var orderedMen4 = repository.Get(spec2);
    }

На мой взгляд это выглядит намного симпатичнее и понятнее, что нам вернется и как будет выглядеть запрос.


Заключение

В заключение, паттерн спецификация и паттерн репозиторий являются мощными инструментами, которые могут помочь создавать более эффективные, удобные и надежные программные системы. Они позволяют облегчить процесс разработки, повысить качество кода и упростить его сопровождение. Для меня они открыли совершенно новый мир, где репозитории создаются по щелчку пальцев и их поддержка не вызывает проблем. Советы, пожелания и рекомендации буду рад увидеть в комментариях.

Источник: https://habr.com/ru/articles/778082/


Интересные статьи

Интересные статьи

Puppeteer — это мощная библиотека автоматизации работы с браузером для веб-скрейпинга и интеграционного тестирования. Однако ее асинхронный реалтайм API оставляет вам достаточно много пространства для...
Перед вами обновлённая коллекция вредных советов для C++ программистов, которая превратилась в целую электронную книгу. Всего их 60, и каждый сопровождается пояснением, почему на самом деле ему не с...
Это глава 19 моей книги «API». v2 будет содержать три новых раздела: «Паттерны API», «HTTP API и REST», «SDK и UI‑библиотеки». Если эта работа была для вас полезна, пожалуйста, оце...
Этим постом я начинаю публикацию v2 моей книги «API». v2 будет содержать три новых раздела: «Паттерны API», «HTTP API и REST», «SDK и UI-библиотеки». Если эта работа была для вас полезна, пожалуйста, ...
Я думаю, что все уже знают мое мнение о MERGE и почему я держусь от него подальше. Но вот еще один антипаттерн, который я постоянно встречаю, когда требуется выполнить UP...