4 марта 2013 в 17:11

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

Предпринимая попытки постичь DDD вы наверняка натолкнетесь на этот паттерн, который часто тесно используется вместе с другим, не менее интересным, паттерном «Репозиторий». Этот паттерн предоставляет возможность описывать требования к бизнес-объектам, и затем использовать их (и их композиции) для фильтрации не дублируя запросы.

Пример


Давайте для примера спроектируем домен для простого группового чата: у нас будут три сущности: Группа и Пользователь, между которыми связь многие-ко-многим (один пользователь может находиться в разных группах, в группе может быть несколько пользователей) и Message представляющий собой сообщение, которое пользователь может написать в какой-либо группе:

public class Member
{
    public string Nick { get; set; }
    public string Country { get; set; }
    public int Age { get; set; }
    public ICollection<Group> Groups { get; set; }
    public ICollection<Message> Messages { get; set; }
}

public class Message
{
    public string Body { get; set; }
    public DateTime Timestamp { get; set; }
    public Member Author { get; set; }
}

public class Group
{
    public string Name { get; set; }
    public string Subject { get; set; }
    public Member Owner { get; set; }
    public ICollection<Message> Messages { get; set; }
    public ICollection<Member> Members { get; set; }
}

Теперь представьте, что вы пишите два метода в Application Service:
/// <summary>
/// Все участники из заданой группы, указавшие заданую страну в профиле
/// </summary>
public ICollection<Member> GetMembersInGroupFromCountry(string groupName, string country)
{
}

/// <summary>
/// Все участники из заданой группы, которые не написал ни одного сообщения в указаный период времени
/// </summary>
public ICollection<Member> GetInactiveMembersInGroupOnDateRange(string groupName, DateTime start, DateTime end)
{
}


Реализация 1 (плохая):


Вы можете позволить сервисам самим строить запросы поверх репозитария:
public ICollection<Member> GetMembersInGroupFromCountry(string groupName, string country)
{	
    return _membersRepository.Query.Where(m => m.Groups.Any(g => g.Name == groupName) && m.Country == country);
}

public ICollection<Member> GetInactiveMembersInGroupOnDateRange(string groupName, DateTime start, DateTime end)
{ 
    return _membersRepository.Query.Where(m => m.Groups.Any(g => g.Name == groupName) && 
      !m.Messages.Any(msg => msg.Timestamp > start && msg.Timestamp < end));
}


Минусы:
Открытие объекта запроса наружу для репозитория сравнимо с открытием магазина торгующего оружием без требования лицензии у покупателей — вы просто не уследите за всеми и кто-нибудь обязательно кого-нибудь пристрелит. Расшифрую аналогию: у вас почти наверняка будет очень много схожих запросов поверх Query в разных частях сервиса(ов) и если вы решите добавить новое поле (к примеру, дополнительно фильтровать группы по признаку IsDeleted, а пользователей по признаку IsBanned) и учитывать его при многих выборках — вы рискуете пропустить какой-нибудь метод.

Реализация 2 (неплохая):


Можно просто описать в контракте репозитария все методы, которые нужны для сервисов спрятав в них фильтрацию, реализация будет выглядить так:
public ICollection<Member> GetMembersInGroupFromCountry(string groupName, string country)
{
    return _membersRepository. GetMembersInGroupFromCountry(groupName, country);
}

public ICollection<Member> GetInactiveMembersInGroupOnDateRange(string groupName, DateTime start, DateTime end)
{ 
    return _membersRepository. GetInactiveMembersInGroupOnDateRange(groupName, start, end);
}


Минусы:
Это реализация лишена недостатка первой, но имеет свой — постепенно ваш репозитарий будет разрастаться и распухать и, в конце концов, превратится во что-то слабо поддерживаемое.

Реализация 3 (отличная):


Тут на помощь к нам и приходит паттерн Спецификация, благодаря которому наш код будет выглядеть так:
public ICollection<Member> GetMembersInGroupFromCountry(string groupName, string country)
{
    return _membersRepository.AllMatching(MemberSpecifications.Group(groupName) && 
        MemberSpecs.FromCountry(country));
}

public ICollection<Member> GetInactiveMembersInGroupOnDateRange(string groupName, DateTime start, DateTime end)
{ 
    return _membersRepository.AllMatching(MemberSpecifications.Group(groupName) && 
        MemberSpecs.InactiveInDateRange(start, end));
}


Получаем двойной профит: репозитарий чист аки слеза младенца и не распухает, а в сервисах нет дублирования запросов и риска пропустить где-нибудь условие, к примеру если вы решите фильтровать группы везде по признаку IsDeleted – вам будет достаточно добавить это условие один раз в спецификации MemberSpecs .FromGroup

Реализация паттерна


Мартин Фаулер (и Эрик Эванс) предложил следующий интерфейс спецификации:
public abstract class Specification<T>
{
    public abstract bool IsSatisfiedBy(T entity);
}

Ребята из linqspecs.codeplex.com cделали его более дружественным к репозитарию (его конкретным реализациям на основе EF, nHibernate и т.п.) и даже серилизуемыми:
public abstract class Specification<T>
{
    public abstract Expression<Func<T, bool>> IsSatisfiedBy();
}


Благодаря ExpressionTree репозиторий сможет распарсить выражение и транслировать его в SQL или во что-либо ещё. Базовая реализация c основными логическими элементами выглядит так:



Теперь нам осталось лишь для каждой сущности (агрегата) написать статический класс, содержащий спецификации. Для нашего примера он будет выглядеть следующим образом:
public static class MemberSpecifications
{
    public static Specification<Member> Group(string groupName)
    {
        return new DirectSpecification<User>(member =>
            member.Groups.Any(g => g.Name == groupName));
    }

    public static Specification<Member> Country(string country)
    {
        return new DirectSpecification<User>(member =>
            member.Country == country);
    }

    public static Specification<Member> InactiveSinceTo(DateTime start, DateTime end)
    {
        return new DirectSpecification<User>(member =>
            member.Messages.Any(msg => msg.Timestamp > start && msg.Timestamp < end));
    }
}


Заключение


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

Ссылки


Отличный пример, откуда были взяты реализации спецификаций находится здесь. Так же обращаю внимание на эту статью на хабре со списком литературы по DDD.
Егор @Nagg
карма
43,5
рейтинг 0,1
Разработчик мобильных приложений
Самое читаемое Разработка

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

  • 0
    Скажите, пожалуйста, зачем вводить отдельный тип «спецификация», если уже есть Func<T, bool> и соответствующий ему expression? Посмотрите на вашу картинку с логическими элементами — вы очередной раз повторяете AST.

    (при этом пользоваться термином никто не мешает, как раз чтобы было понятно, какой шаблон из DDD мы применяем)
    • 0
      Вот сама сижу и думаю :)

      Отдельный класс «Спецификация», скорее всего нужен для того, что бы собрать в одном месте условия такие как: Валидный пользователь, Все доступные для обработки и так далее.

      К примеру: если членство в группе у нас определяется сейчас не только связью «член-группа», а допустим ещё каким-то критерием, скажем временем.

      В проекте мы использовать будем в первое время (Func<Member, bool>) x=>x.GroupName == groupName, и будем использовать это 100500 раз, потом появиться новое требование: членство в группе считается только если группа совпадает, и _связь была непозднее чем 1 месяц назад_

      Реально, что бы это реализовать прийдётся найти все упоминания где мы используем x=>x.GroupName == groupName и добавить туда ещё что-то вроде следующего:

      x=>x.GroupName == groupName && x.MemberSince < DateTime.Now.Substruct(TimeSpan.FromDays(30));

      Используя «Спецификацию», нужно будет подправить только спецификацию. Как-то так.

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

        Создаем статический класс, у которого будут статические же свойства IsValidUser, AllValidFor с типами Expression<Func<User,bool>> и так далее.

        (в случае использования класса «спецификация» картина та же, только типы свойств другие; собственно, в примере выше он называется MemberSpecifications)
      • +2
        Напишите x=>x.GroupName == groupName один раз и засуньте его в статический метод-расширения с говорящим названием:

        public class UserQueryExtensions 
        {
           public static IQueryable<User> WhereGroupNameIs(this IQueryable<User> users, strin name) 
           {
               return users.Where(u => u.GroupName == name);
           }
        }
        


        В 10 раз меньше кода, а результат тот же.
    • 0
      Лямбды такие противные штуки — их нельзя наследовать, с ними нельзя проводить арифметические операции, они не поддаются кастам, посмотрите в моем примере на строку:
      AllMatching(MemberSpecifications.Group(groupName) && MemberSpecs.InactiveInDateRange(start, end)
      В вашем решении мы получим класс с жутковатого вида конструкциями типа
      public static Expression<Func<Message, bool>> SomeQuery = message => ...;
      на все случае жизни
      • +1
        Наследование тут, как мы понимаем, достаточно бесполезная штука — потому что в итоге реализуется так же, как и соответствующие вещи на выражениях. Кастовать тоже не очень понятно, зачем. А что касается логических операций (вы ведь их имели в виду, зачем вам арифметические операции над предикатами) — то они решаются написанием трех хелперов.

        Если бы у вас были чистые спецификации (которые bool IsSatisfiedBy()), то было бы еще понятно. Но когда у вас спецификация возвращает все то же AST, смысл этого уже не очень понятен, потому что вам все равно придется внутри обрабатывать этот AST (собственно, вот вам и протекшая абстракция).
        • +1
          К тому, что написал выше добавлю ещё, что когда где-нибудь в коде (не только репозитарии) увижу аргумент ISpecification я сразу пойму где мне искать уже готовые запросы через Go to implementation, в вашем же случае новичок увидит аргумент Expression<Func<T, bool>> ужаснется и скорее всего будет писать собственный запрос (лямбду) и даже не потрудится поискать готовый. В этом и прелесть шаблонов проектирования.
          • +1
            Угу. Замечательная прелесть — заставить человека написать лишний код там, где он не нужен.

            (не говоря уже о том, что в linqspecs есть AdHocSpecification как раз для таких случаев)
            • 0
              Как-раз в моем примере человек сам напишет новую большую лямбду вместо поиска уже готовой (или композиции нескольких) спецификаций.
              • 0
                Вы почему-то исключаете варинты «напишет новую большую спецификацию вместо поиска» и «напишет ad-hoc-спецификацию».
                • +1
                  Увидев ISpecification в параметрах репозитория любой разработчик именно так и сделает. Чихал он на уже существующие реализации.
    • +4
      Потому что ваш способ плохо тестируем и не понятен, в случае усложнения. В реальной практике в лямбде понятны 1-2 параметра, в LINQ 3-4
      Большее количество начинает вызывать желание пригласить писателя, даже если он уволился. Всему своё место и время. Применение спецификации заключается не только в запросах, как описал автор топика. Более того, есть мнение, что передача спецификации в запрос (хранение запроса в спецификации) имеет не только плюсы но и минусы.
      Но с другой стороны как мы говорим? «Я хочу получить детали соответствующие вот такой спецификации (болт 10х3 крестовой, стальной)». Закатал всё это в спецификацию и передал в запрос Nhibernate или EntityFramework:
      repository.GetBySpec(BoltSpecification)

      Удобство применения заключается ещё и в обработке бизнес-логики.

      Пример:
      получили мы список клиентов, которых нужно по 2м группа рассортировать? А критериев сортировки ни 1 ни 2, а десяток. И вместо 2х запросов к БД можно сделать 1 запрос и в памяти разбить на группы вот так:

      myCustomers = getCustomersFromDB(queryRestrictions);
      foreach(itemin myCustomers )
      {
      if(Spec1.IsSatisfied(item))
      {
      group1.Add(item)
      }
      elseif(Spec12IsSatisfied(item))
      {
      group2.Add(item)
      }
      }

      И всё. И этот код соответствует домену и SOLID. Он не должен отвечать за критерии разделения кастомеров. Он должен отвечать только за то, чтобы кастомеры попали в нужную группу, а по каким критериям? Нам спецификация пускай отвечает с единым интерфейсом
      Или ещё,
      Если мне нужно проверить сущность на соответсвие, то я просто проверяю это через:
      if(ListOfCustomersToDeleeteSpecification.IsSatisfiedBy(myList)) {....}

      Вместо очередного копипаста в коде if (customer.State == OLD && customer.Orders.Count == 0 &&… && ....)

      При этом всё отлично наследуется\делегируется. И одна спецификация может быть собрана из 10 других. А код при этом будет простой. Потому что спецификация, она как и на производстве — условия соответствия объекта условиям которые явно задекларированы. А не просто набор ифов.

      И лябмды, особенно когда чуть сложнее 2-3 запросов это просто жесть. И когда читаешь чужой код, то хочется руки вырвать предшественнику за простыню вызовов функций, присвоений, проверок, снова вызовов…

      Все вопросы зачем и почему и даже минусы изложены подробно в книге по DDD Эрика Эванса с примерами. Только чтобы понять высшую математику (в данном случае применение и удобство этого паттерна как раз высшая математика), сперва стоит понять алгебру с геометрией (прочитать книгу, а не одну главу про паттерн). Потому что нельзя, прочитав пару статей на хабре всё понять, и сразу (как мы привыкли) получить все плюсы от удобства и гибкости DDD, взяв от туда только громкие названия, и снова закатов всё в ActiveRecord с кучей несвойственного поведения.

      Сам использую этот паттерн постоянно и другие паттерны в связке: Factory, Service, Repository, Entity. 90% бизнес логики ими покрывается. Удобство возросло в разы, а количество кода уменьшилось. Исправления в код вносятся очень быстро, гибкость тоже достаточна. И главное — никаких велосипедов не придумано, просто DDD это пример того, о чем писали и пытались научить нас Фаулер и Гамма.
      • +4
        О боже, в который раз…

        Книги Эванса (DDD, 2003) и Фаулера (PoEAA, 2003) были написаны 10 лет назад, когда небыло никаких ORM и миром правили ADO.NET и JDBC, тогда люди писали код на SQL и дергали его из кода на .NET/JAVA, либо держали логику в хранимых процедурах (множество «программистов» до сих пор так делает).

        Дак вот, тогда, в 2002-2003 году, паттерн «спецификация» имел смысл для самостоятельной реализации. Но потом появились ORM, которые были спроектированны по этим книгам (Hibernate/NHibernate содержит внутри себя все патерны описанные в PoEAA, относящиеся к БД).

        Так вот,

        — ICriteria из NHibernate это уже паттерн «спецификация» + «Объект запроса»
        — IQueryable`1 это уже паттерн «спецификация» + «Объект запроса»

        Зачем городить свой, вводя дополнительные абстракции, поверх существующего — мне не понятно.
        • 0
          А вы не думали что ICriteria и IQueryable (в меньше степени) это привязка к конкретной ORM?
          • +1
            Нет не думал. Это факт. Что плохого в более тесной интеграции с ORM? При правильном абстрагировании вы получаете большую гибкость и большую степень интеграции.

            Ваш способ абстрагирование — привносит дополнительную бесполезную абстракцию, которая, к тому же, раскрывает детали реализации и еще больше привязывает к конкретному механизму запросов (в данном случае — LINQ).

            Т.е. при гипотетической смене ORM, о которой вы неявно упомянули, речи быть не может. т.к. вы не сможете комбинировать «спецификации», которые оперируют с разными ORM. Вам придется либо за один присест пере- или дописать все реализации ваших спецификаций под новую ORM, добавть ISpecificationFactory, который будет создавать все известные спецификации для данной ORM (тут я не вижу никаких отличий от «распухшего» репозитория) и следить, чтобы не дай бог, разные реализации не взаимодействовали между собой.

            К тому же, при такой теоретической смене ORM, с большой долей вероятнсти, нельзя будет перенести существующие спецификации один-к-одному в новую реализацию.
          • 0
            Давайте использовать ORM для абстрагирования от БД, а затем ещё какие-то абстракции для абстрагирования от ORM. Ага. Часто вам приходилось менять БД\ORM в проекте? Уверен, что никогда. В т.ч. из-за того, что у всех БД/ORM есть свои собственные нюансы, которые необходимо учитывать и тяжело (да и незачем) запрятать в глубь абстракций.
            • 0
              Часто вам приходилось менять БД\ORM в проекте? Уверен, что никогда.

              DAL за время жизни одного проекта — трижды, из них один раз — ORM. БД за время жизни того же проекта — как минимум единожды.
              • 0
                Раскройте секрет — по каким причинам? Интересно
                • 0
                  Причина типичная: из-за текучки кадров приходит новый «архитектор» и говорит, что все это нужно переписать.
                • –1
                  Потому что унаследованный проект, который последовательно двигали на более удобную для команды модель.

                  А БД — по требованиям заказчика.
          • 0
            • 0
              Я тоже знаю много классных аббревеатур.
      • 0
        Потому что ваш способ плохо тестируем и не понятен, в случае усложнения.

        А как тестировать предложенный автором способ (напомню, у него SatisfiedBy возвращает выражение, а не bool)?

        И вместо 2х запросов к БД можно сделать 1 запрос и в памяти разбить на группы вот так:

        Если мне нужно проверить сущность на соответсвие, то я просто проверяю это через:

        Этот код не будет работать с той реализацией спецификации, которую предлагает автор (и к которой я и задаю вопросы).

        И одна спецификация может быть собрана из 10 других.

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

        Сам использую этот паттерн постоянно и другие паттерны в связке: Factory, Service, Repository, Entity.

        По-вашему, Entity — это паттерн?
        • 0
          Покажите мне, пожалуйста, как вы соберете спецификацию из десяти других, если каждая из них возвращает Expression<Func<T,bool>>.

          public static Specification Group(string groupName)
          {
          var spec = new DirectSpecification(member => member.Groups.Any(g => g.Name == groupName));
          spec &= DeletedGroup();
          spec &= ApprovedGroup();

          return spec;
          }
          По-вашему, Entity — это паттерн?

          Возможно, имелся ввиду layer supertype

          • 0
            public static Specification Group(string groupName)
            {
            var spec = new DirectSpecification(member => member.Groups.Any(g => g.Name == groupName));
            return spec & DeletedGroup();
            }
            


            Не, это смешно, как мы понимаем. Покажите мне логику внутри перегруженного оператора &.

            (а то мы сейчас придем к тому, что для выражений тоже можно использовать firstExpresion.And(secondExpression) и поспорим о том, что читабельнее — операторы или fluent)
            • 0
              Не понял что тут смешного: вы спросили — я ответил, если бы вы прочитали статью и прошли по предложенной ссылки на linq specs вы бы не задали такого вопроса. Если кратко, то под & (или And в fluent стиле) скрывается firstExpression.Compose(secondExpression, Expression.And)
              • 0
                Смешно здесь то, что решение, построенное на expressions (безо всяких спецификаций), точно так же позволяет компоновать выражения из составляющих. Поэтому достаточно странно преподносить это как уникальное достоинство решения с выделенным классом для спецификаций.

                По факту, ваша композиция спецификаций — и есть композиция их выражений, просто добавлена абстракция поверх.
  • +1
    Почему у первых двух решений описаны минусы, а у последнего — нет?

    Какая разница между статическим классом, содержащим спецификации и репозиторием (который к тому же не статический)?
  • 0
    А почему бы просто не хранить Expresion<Func<T, bool>> в поле (статическом), зачем классы?

    Или еще лучше иметь методы IQueryable -> IQueryable, которые фильтрую, отображают, группируют или делают еще чтонить полезное.

    Например
    public interface IVisible
    {
        public bool IsVisible { get; set; }
    }
    static class Program
    {
    
        static IQueryable<T> Visible<T>(this IQueryable<T> query) where T : IVisible, new()
        {
            return query.Where(x => x.IsVisible);
        }
    
        static void Main(string[] args)
        {
            var ctx = new MyDBContext();
            var docs = from d in ctx.Documents
                       where d.Author == "currentUser"
                       select d;
            docs = docs.Visible();
        }
    }
    
  • 0
    Linq и всяческие спецификации, которых на просторах сети бесчисленное множество — текущие (leaked) абстракции.

    Вот вам простой пример: необходимо добавить полнотекстовый поиск по полю GroupName (ну или любое другое поле — не суть) и вывести всех онлайн пользователей.

    Ваши действия?
    • 0
      Мои действия просты, создаю спецификацию как в тексте статьи с лямбдой group => group.Name.Contains(groupName); — где тут текучесть?
      • 0
        Вы понимаете разницу между полнотекстовым поиском (FULLTEXT SEARCH) и поиском по подстроке?
        • 0
          Вы можете какой угодно метод использовать\придумать и в конкретном репозитарии разобрать выражение и выполнить нужный поиск\запрос.
          • 0
            Причем тут репозитории?
            • 0
              Притом. Разбор спецификации и трансляция в SQL (если мы уже говорим об SQL) происходит в реализации репозитария. Обычно это делается по средствам какой-нибудь известной ORM типа EF или nHibernate.
              • 0
                Покажите мне код для конкретно моего примера.

                PS: Откуда вы вообще взяли слово «репозитарий»?
          • 0
            «Довольно болтовни, покажите мне код» © такой спецификации.
    • 0
      Чутка не понял задачи и что Вы хотите доказать…

      var adminsOnline = repository.Where(new UserInGroup("admins").And(new UserOnline()));
      
      • 0
        Да нет, ничего, просто полнотекстовый поиск не поддерживается популярными LINQ-провайдерами из коробки.
        • 0
          Справедливости ради надо отметить, что проблема тут не в паттерне «спецификация», а в LINQ-провайдерах.
          • 0
            А вы статью-то читали? У автора там эти спецификации обёртка над LINQ, который сам уже реализация этого паттерна. Вот я и задаю ему вопрос, как он из такой ситуации будет выкручиваться.
            • 0
              (Я не только читал, я еще и комментировал этот конкретный пункт.)

              Выкручиваться из этой ситуации достаточно «просто» — написать свой провайдер или расширить существующий.
            • 0
              Маппинг на функцию делать, а в ней что хочешь на SQL. Linq все таки абстрагирует от работы с хранилищем. Поэтому напрямую не будет поддерживать многие вещи.
  • +1
    А я голосую за явные контракты у репозитория. Спецификациям имхо место в слое бизнес-логики, для проверки условий и прочего.
    А когда мне нужно выбрать из хранилища объекты, удовлетворяющие конкретным условиям — мне будет удобно не заморачиваться поиском и использованием конкретной спецификации (а чем вам ICriteria из NH не угодил? Зачем поверх него ещё что-то? :-)), а просто воспользоваться явным контрактом этого хранилища, просто передав туда нужные параметры. А оно там пускай само решает, как именно будет искать эти данные…
    • 0
      Я описал минусы этого решения — толстые репозитории, в решении со спеками у вас может получится воспользоваться генерик репозитарием для всех агрегатов.
      • 0
        А я не согласен, что мифические (!) толстые репозитории — это минус. Это контракт, а не минус.

        При этом куча спецификаций, generic-репозиторий — это большой набор абстракций. А абстракции плохо справляются с частными случаями.
        А если мне нужно получить кол-во элементов по условию? А если этот запрос слишком монстроидальный и в БД есть специальные колонки для кэширования count'ов? А если, получая коллекцию с кучей ассоциаций, я не хочу напороться на SELECT N+1 и в запросе необходимо указать правильный выбор JOIN или SUBSELECT? А если при выборке по условию какие-то ассоциации необходимо загрузить сразу, минуя дефолтный lazy-load? Как обобщенные репозитории и интерфейс ISpecification ответят на эти «если»?
        • 0
          JOIN вообще плохо ложится на репозиторий. Или Вы держите весь контекст, чтобы иметь доступ ко всем сетам?
          А с SUBSELECT вопрос интересный. И в случае с простым репозиторием приводит к копированию запроса, но с другим SUBSELECT (я исключаю случай, когда возвращается IQueryable в принципе). В случае спецификации добавляется еще один параметр в репозиторий, который влияет на SUBSELECT. Таким образом мы можем не смешивать логику запроса и загрузки.
        • 0
          >> я голосую за явные контракты у репозитория.

          «Загрузи сущности, удовлетворяющие спецификации» и есть явный контракт.
          Проблема в том, что давать наружу ICriteria/IQueryable чревато потерей контроля над дао-слоем (как раз случай — сделали колонки для подсчёта count-ов, а клиентский код по прежнему считает кол-во на сервере).
          Не давать наружу ICriteria/IQueryable и не использовать спецификации — распухает интерфейс дао (на каждый чих — свой метод). А в случае со спецификациями распухают контракты спецификаций, что легче поддерживать.

          >> А если мне нужно получить кол-во элементов по условию?

          Делаем два метода — «загрузи мне объекты по спецификации» и «посчитай кол-во объектов, удовлетворяющих спецификации».
          Методы могут либо опираться на общий код построения критерии, в конце лишь добавляя проекцию count(), либо на абсолютно разные алгоритмы (ага, count-ы в отдельных столбцах) — слой выше код не должен видеть детали.

          >> А если, получая коллекцию с кучей ассоциаций, я не хочу напороться на SELECT N+1 и в запросе необходимо указать правильный выбор JOIN или SUBSELECT?

          Добавляем хинты загрузки в спецификацию.

          >> А если при выборке по условию какие-то ассоциации необходимо загрузить сразу, минуя дефолтный lazy-load?

          Аналогично. И остаёмся в рамках абстракции — список юзеров и список юзеров с подгруженной группой — разные списки, соответствтенно на них нужны разные спецификации.

          >> Как обобщенные репозитории и интерфейс ISpecification ответят на эти «если»?

          Имхо, бизнес-слой не должен видеть, что спецификация построена на общем интерфейсе или имеет AST внутри.
          Да и строгое следование единой структуре — плохо, потому что ограничивает стратегии применения спецификации.

          Удобно в спецификации давать ссылки на другие спецификации. Например, спецификация группы может содержать ссылку на спецификацию юзера, что трактуется как «найди мне группы, где есть юзеры с указанной спецификацией».
          • +1
            Поздравляю, вы только что, отвечая на мои вопросы, переизобрели критерии от NH. Которые сами же и обозвали какой.
            • 0
              Критерии NH не знают, что посчитанные count-ы можно взять из отдельной таблицы.
              Критерии NH не знают, что по умолчанию не нужно грузить юзеров с флагом IS_DEAD=1

              Возможно, это головная боль бизнес-логики, а не хранилища. Но мне нравится умный дао-слой.
              • 0
                Я вас не понимаю, вообще.

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

                1) Кода будет ровно столько же с точностью до сигнатур новых классов. Это очевидно.
                2) В добавок мы дали пользователям возможность создавать собственные спецификации в любой части приложения. Любой новичок в проекте, не знающий где и что искать — будет писать своё. Это опыт.
                3) Данный подход ограничивает пользователя до «что я хочу получить», не давая задать «как именно я хочу это получить». Мне в приложении нужно загрузить пост и отрисовать его комментарии. Я чётко знаю, как я буду пользоваться результатом выборки. Обобщённый подход приведёт к select n+1. Поэтому мне нужен JOIN. Что делать? Это суровая реальность.

                Пожалуйста, объясните на пальцах, где же выгода от данного подхода. И как решать упомянутые выше проблемы.
                • 0
                  Кода будет меньше. Если отвлечься от авторского велосипеда, а посмотреть на спецификации вообще, но мы получаем:

                  1. Петя хочет загрузить список групп, в названии которых есть слово «админ». Он либо пишет специальный метод с загрузкой групп по указанной подстроке, либо учит спецификацию выражать это требование.
                  2. Вася хочет загрузить список первых 10 групп, в которые были новые посты, с сортировкой по дате поста. Он дописывает в спецификацию фичу сортировки по дате и ограничения по кол-во.
                  3. А теперь Коля хочет взять первые 20 групп с названием, содержащим «Авто», в которых были новые посты. Вот и экономия — спецификация уже позволяет скомбинировать фичи Пети и Васи.
                  Рано или поздно спецификация перестаёт бурно расти и может удовлетворить любые комбинации.

                  Почему не хочется в бизнес-слой отдавать критерию — писали выше.
                  Могу добавить юз-кейс: допустим, надо читать данные не одним селектом, а страницами по 1000. Не спрашивайте почему, ну вот надо и всё тут — ограничение нижележащего уровня. В загрузчике сущностей по спецификации это требование нужно закодить только один раз, если отдавать наружу критерии — это требование будет размазано по бизнес-логике и наверняка с ошибками (Коля подумает — а нафига мне здесь это делать, в моём случае уж точно не более 1000 будет прочитано — и ошибётся в этом прогнозе).
                  • 0
                    Не поймите меня неверно, я не предлагал отдавать наружу ICriteria, это неразумно. Автор предложил интерфейс ISpecification, который и только который умеет приниматься единственным методом репозитория. По мне это то же самое, что принимать на вход ICriteria. Разницы — нет. Но при этом автор предлагает внутренний код спецификаций считать декларативным описание запроса и из этого генерировать соответствующий запрос к репозиторию. То, что это работает не всегда, мы знаем по опыту с SQL. Отсюда и возник мой вопрос: а зачем тратить время, если всё это под другими названиями уже итак есть и всё-равно будет иметь заранее известные границы применимости?

                    А вот «Вася дописывает фичу сортировки по дате и ограничение на кол-во» — уже похоже на реальную жизнь. И здесь уже не будет «распарсим декларативное описание спецификации и сгенерим соответствующий запрос к репозиторию в общем виде». Здесь уже будет императивный код, как эта конкретная спецификация транслируется в низкоуровневое API слоя данных. С сортировками, лимитами, стратегиями загрузки ассоциаций и т.д. А это уже называется Query Object и является отличным выходом из ситуации, да.
                    • +1
                      Ох, похоже я эти паттерны cпутал :)
                      • 0
                        Бывает :)
        • 0
          Создать View и к ней orm'ить
  • +1
    Полезный паттерн, надо будет взять на вооружение. Как раз недавно добавили в проекте к сущности новый признак isActive и не поправили выборку в одном из методов, в результате чего система продолжала тихо работать с не активными объектами. Баг обнаружили не сразу.
  • +4
    Всегда, при рассмотрении любого шаблона, найдется добрый пользователь, который напишет — «Напришите вот так то, и будет тоже самое и в 10 раз меньше кода». И все бы ничего, но с такими приходится еще и работать…
  • +1
    Для задачи в том виде, что описана в посте, проще использовать IQueryable.Where(Expression) msdn. Об этом еще раз говорит использование DirectSpecification в последнем участке кода. Specification кстати выглядит простоватой надстройкой над использованием Expression.
    Нужно смириться с тем, что с приходом Expression от спецификации не будет какой-то ощутимой пользы.
  • 0
    Помню в делёком 2006 году вводил в свой проект подобный шаблон, но тогда не было expression и linq поэтому все условия формировались через компоновку чистого sql запроса.
    В итоге получилась довольно не поворотливая система которую никто из команды не желал поддерживать (просто все забывали где есть нужная спецификация, а где ее нет, новички вообще плавали/дымили в таких запросах).
    То что я вижу сейчас довольно красиво оформлено и абстрагировано, но на мой взгляд пригодно только для больших команд (где все четко формализовано, документировано и разделены обязанности)
    • +1
      Это касается любого шаблона, были случаи когда люди даже фабрики не переваривали, просили вернуть назад switch в код.
  • 0
    Третий подход может и отличный, не то, что первый, но в первом зато вроде нет багов. Зато третий возвращает инвертированное значение для InactiveSinceTo.

    А по делу, автор говорит, что первый подход плох в том, числе тем, что
    … если вы решите добавить новое поле (к примеру, дополнительно фильтровать группы по признаку IsDeleted, а пользователей по признаку IsBanned) и учитывать его при многих выборках — вы рискуете пропустить какой-нибудь метод.


    Третий же подход «отличный», но как он адресует эти проблемы не ясно. Сказано только про группы, а как быть если пользователей вдруг стало нужно фильтровать по IsBanned==false?

    Варианты:

    1. Добавляем новую спецификацию NotBanned — ее получается надо добавлять во все запросы, т.е. тот же минус, что и у исходного подхода.

    2. Добавляем фильтр по IsBanned==false куда-нибудь в AllMatching. Тогда получается это не спецификации решают эту проблему, а какой-то сопутствующий подход, который мы могли спокойно и в изначальном коде применить.

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

    Так что делать?
    • 0
      Продолжать пользоваться 2м подходом, ИМХО

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.