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

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

  • Минимальный API;

  • Не использовать методы расширения;

  • Совместимость с существующим кодом;

  • Использование как с провайдерами баз данных так и просто в обычном коде;

  • Композиция;

  • Не должно быть привязано ни к какому фреймворку;

Основные ограничения существующих решений:

  • Неоправданно объемные определения (например, надо надо создавать целый отдельный класс);

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

  • Лишний функционал вроде поддержки пагинации или сортировки;

  • Фокус только на деревьях выражений либо только на варианте с методами/делегатами;

  • Лишние компиляции делегатов без намека на оптимизацию;

  • Слабые возможности по композиции, например, вложенные условия не поддерживаются;

  • Возможно использовать только в некоторых контекстах;

Может возникнуть вопрос: Почему просто не использовать набор методов расширений или даже просто выражения (Expression<Func<T, bool>>)? Разумеется, есть случаи когда и этого будет достаточно, но часто одни и те же условия необходимо проверять как при запросе в базу, так и в обычном коде, поэтому очевидно, что надо поддерживать оба сценария.

Еще один вариант - создать некий сервис(ы), в котором будут методы вроде IsUserActive, IsUserRegistered и так далее. Опять же, в каких-то случаях это тоже оправдано, но с композицией и переиспользованием у такого подхода может быть еще хуже. Могут быть условия, которые просто не возможно проверить в одном запросе или внутри спецификации, но эти проблемы можно решить или сгладить.

Перейдем к примерам объявления:

public class User
{
    public int Id { get; set; }

    public string Name { get; set; }

    public bool Active { get; set; }

    public Subscription Subscription { get; set; }

    public List<Department> Departments { get; set; }
}

public class Department
{
    public int Id { get; set; }

    public string Name { get; set; }

    public bool Active { get; set; }
}

public enum Subscription
{
    Subscribed,

    Unsubscribed
}

public static class Specifications
{
    // в базе сравнение не учитывает регистр
    public static readonly Specification<Department> CustomerServiceDepartment = new(
        x => x.Name == "Customer Service",
        x => string.Equals(x.Name?.TrimEnd(), "Customer Service", StringComparison.InvariantCultureIgnoreCase)
    );

    // делагат скомпилируется при вызове
    public static readonly Specification<Department> ActiveDepartment = new(x => x.Active);

    public static readonly Specification<User> ActiveUser = new(
        default,
        x => x.Active // передаем только делегат, выражение будет вычислено
    );

    // инвертируем
    public static readonly Specification<User> InactiveUser = !ActiveUser;
    
    // комбинируем
    public static readonly Specification<User> SubscribedUser = ActiveUser && new Specification<User>(x => x.Subscription == Subscription.Subscribed);

    public static readonly Specification<User> VasiliyUser = new(x => x.Name == "Vasiliy");

    // можем даже использовать спецификации внутри других спецификаций
    public static readonly Specification<User> UserInCustomerServiceDepartment = new(x => x.Departments.Any(CustomerServiceDepartment && ActiveDepartment));
}

Упрощенный пример использования:

public class UserController : Controller
{
    private readonly DbContext _context;

    public UserController(DbContext context)
    {
        _context = context;
    }

    // option 1: DB
    public Task<User> GetUser(int id)
    {
        return _context.Set<User>()
            .Where(Specifications.UserInCustomerServiceDepartment)
            .Where(x => x.Id == id)
            .SingleOrDefaultAsync();
    }

    // option 2: in-memory
    public async Task<User> GetUser(int id)
    {
        var user = await _context.Set<User>()
            .Include(x => x.Departments)
            .Where(x => x.Id == id)
            .SingleAsync();

        return Specifications.UserInCustomerServiceDepartment.IsSatisfiedBy(user) ? user : null;
    }
}

Основные инструменты - деревья выражений + визиторы и библиотека DelegateDecompiler.

Весь код доступен на гитхабе. Он включает в себя еще и проекции из моей другой статьи.

Если лень читать код - можно установить пакет из nuget и попробовать у себя.

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


  1. Dotarev
    04.07.2023 06:33
    +4

    Что есть Specification<T>? Чтобы понять, надо заглянуть на GitHub?


    1. kemsky Автор
      04.07.2023 06:33
      -3

      Именно так, реализация слишком тривиальна, чтобы размещать ее в статье.


  1. Dotarev
    04.07.2023 06:33

    Offtop: Допустимо ли правилами Хабра размещать статью о своей библиотеке где-то, кроме хаба "Я пиарюсь"? Это не упрек, сам хотел бы такое сделать, но насколько я понял - чревато баном.


    1. moderator
      04.07.2023 06:33
      +1

      Допустимо. Правило про размещение статей в хабе "Я пиарюсь" распространяется только на публикации в которых рассказывается про коммерческий продукт, услугу, сервис, мероприятие.


  1. nronnie
    04.07.2023 06:33
    +5

    В статье ни слова ни про сам паттерн, ни про какие-либо (интересные или необычные) детали его реализации. Все можно было бы уложить в одно предложение: "Посмотрите, как я реализовал паттерн спецификация: <ссылка на GitHub>".


    1. kemsky Автор
      04.07.2023 06:33
      -2

      В первом же предложении есть целых три ссылки в которых рассказывают, что же такое спецификация и я не вижу смысла повторять содержимое.


      1. Vaulter
        04.07.2023 06:33
        +2

        А стоило бы резюмировать.


  1. zartarn
    04.07.2023 06:33

    Про спецификацию хорошо было написано у marshinov https://habr.com/ru/articles/325280/ + смежная статья https://habr.com/ru/articles/313394/