Является ли шаблон спецификации бессмысленным? - PullRequest
10 голосов
/ 15 декабря 2010

Мне просто интересно, является ли шаблон спецификации бессмысленным, учитывая следующий пример:

Скажем, вы хотите проверить, достаточно ли у Клиента достаточного остатка на его / ее счете, вы бы создали спецификацию, например:1003 *

new CustomerHasEnoughMoneyInTheirAccount().IsSatisfiedBy(customer)

Однако мне интересно то, что я могу добиться тех же «преимуществ» шаблона спецификации (например, что мне нужно только изменить бизнес-правила на месте) с помощью средства получения свойств в классе Customer, например:это:

public class Customer
{

     public double Balance;

     public bool HasEnoughMoney
     {
          get { return this.Balance > 0; }
     }
}

Из кода клиента:

customer.HasEnoughMoney

Так что мой вопрос действительно таков;В чем разница между использованием свойства get для переноса бизнес-логики и созданием класса спецификации?

Заранее всем спасибо!

Ответы [ 4 ]

9 голосов
/ 15 декабря 2010

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

Этот шаблон действительно вступает в свои права, когда высоздание чего-то более сложного, такого как:

var spec = new All(new CustomerHasFunds(500.00m),
                   new CustomerAccountAgeAtLeast(TimeSpan.FromDays(180)),
                   new CustomerLocatedInState("NY"));

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

Тем не менее, C # предоставляет более идиоматические способы выражения такого рода вещей, такие как методы расширения и LINQ:

var cutoffDate = DateTime.UtcNow - TimeSpan.FromDays(180); // captured
Expression<Func<Customer, bool>> filter =
    cust => (cust.AvailableFunds >= 500.00m &&
             cust.AccountOpenDateTime >= cutoffDate &&
             cust.Address.State == "NY");

Я играл с экспериментальным кодом, который реализует спецификации в терминах Expression s, с очень простыми статическими методами построения.

public partial class Customer
{
    public static partial class Specification
    {
        public static Expression<Func<Customer, bool>> HasFunds(decimal amount)
        {
            return c => c.AvailableFunds >= amount;
        }

        public static Expression<Func<Customer, bool>> AccountAgedAtLeast(TimeSpan age)
        {
            return c => c.AccountOpenDateTime <= DateTime.UtcNow - age;
        }


        public static Expression<Func<Customer, bool>> LocatedInState(string state)
        {
            return c => c.Address.State == state;
        }
    }
}

Тем не менее, это целая нагрузка, которая не добавляет ценности! Эти Expression смотрят только на общедоступные свойства, так что можно так же легко использовать простую старую лямбду!Теперь, если одной из этих спецификаций требуется доступ к непубличному состоянию, нам действительно do нужен метод компоновщика с доступом к непубличному состоянию.Я буду использовать lastCreditScore в качестве примера здесь.

public partial class Customer
{
    private int lastCreditScore;

    public static partial class Specification
    { 
        public static Expression<Func<Customer, bool>> LastCreditScoreAtLeast(int score)
        {
            return c => c.lastCreditScore >= score;
        }
    }
}

Нам также нужен способ составить из этих спецификаций - в данном случае, составной, который требует, чтобы все дочерние элементы были истинными:

public static partial class Specification
{
    public static Expression<Func<T, bool>> All<T>(params Expression<Func<T, bool>>[] tail)
    {
        if (tail == null || tail.Length == 0) return _0 => true;
        var param = Expression.Parameter(typeof(T), "_0");
        var body = tail.Reverse()
            .Skip(1)
            .Aggregate((Expression)Expression.Invoke(tail.Last(), param),
                       (current, item) =>
                           Expression.AndAlso(Expression.Invoke(item, param),
                                              current));

        return Expression.Lambda<Func<T, bool>>(body, param);
    }
}

Полагаю, что недостатком этого является то, что это может привести к сложным деревьям Expression.Например, построение этого:

 var spec = Specification.All(Customer.Specification.HasFunds(500.00m),
                              Customer.Specification.AccountAgedAtLeast(TimeSpan.FromDays(180)),
                              Customer.Specification.LocatedInState("NY"),
                              Customer.Specification.LastCreditScoreAtLeast(667));

создает дерево Expression, которое выглядит следующим образом.(Это слегка отформатированные версии того, что ToString() возвращает при вызове на Expression - обратите внимание, что вы не сможете увидеть структуру выражения вообще, если у вас будет только простой делегат! Пара замечаний:DisplayClass - это класс, сгенерированный компилятором, который содержит локальные переменные, захваченные в замыкании, для решения проблемы funarg вверх , а в дампе Expression используется один знак = для представления сравнения на равенство, а не типичный C # ==.)

_0 => (Invoke(c => (c.AvailableFunds >= value(ExpressionExperiment.Customer+Specification+<>c__DisplayClass0).amount),_0)
       && (Invoke(c => (c.AccountOpenDateTime <= (DateTime.UtcNow - value(ExpressionExperiment.Customer+Specification+<>c__DisplayClass2).age)),_0) 
           && (Invoke(c => (c.Address.State = value(ExpressionExperiment.Customer+Specification+<>c__DisplayClass4).state),_0)
               && Invoke(c => (c.lastCreditScore >= value(ExpressionExperiment.Customer+Specification+<>c__DisplayClass6).score),_0))))

Грязно!Много вызовов непосредственных лямбд и сохраненных ссылок на замыкания, созданные в методах компоновщика.Путем замены ссылок закрытия их захваченными значениями и β-редукцией вложенных лямбд (я также α-преобразовал всех имен параметров в уникальные сгенерированные символы в качестве промежуточного шага для упрощения β-редукции), гораздо более простое Expression дерево результатов:

_0 => ((_0.AvailableFunds >= 500.00)
       && ((_0.AccountOpenDateTime <= (DateTime.UtcNow - 180.00:00:00))
           && ((_0.Address.State = "NY")
               && (_0.lastCreditScore >= 667))))

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

В дополнение к этому, я построил небольшой глупый микробенчмарк и фактически обнаружил, что устранение эталонного замыкания оказало заметное влияние на производительность на скорость оценки.примера Expression при компиляции для делегата - это сократило время оценки почти вдвое (!), с 134,1 нс до 70,5 нс за вызов на машине, с которой я случайно сижу.С другой стороны, β-сокращение не оказало заметного влияния, возможно, потому что компиляция делает это в любом случае.В любом случае, я сомневаюсь, что обычный набор классов спецификации может достичь такой скорости оценки для комбинации из четырех условий;если бы такой традиционный набор классов должен был быть построен по другим причинам, таким как удобство кода пользовательского интерфейса, я думаю, что было бы целесообразно, чтобы набор классов производил Expression, а не непосредственно оценивал, но сначала подумайте, нужно ли вамшаблон вообще в C # - я видел слишком много перегруженного спецификациями кода.

8 голосов
/ 15 декабря 2010

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

3 голосов
/ 30 сентября 2016

Да, это бессмысленно.

Статья в Википедии подробно критикует эту модель. Но наибольшую критику я вижу только из-за эффекта внутренней платформы . Зачем заново изобретать оператор AND? Снова, прочитайте статью Wikipedia для более полной критики.

Генри, вы правы, если предположите, что Property Get превосходит. Зачем отказываться от более простой, хорошо понятной концепции ОО для неясного «шаблона», который в своей концепции не отвечает самому вашему вопросу? Это идея, но плохая. Это анти-шаблон, шаблон, который работает против вас, кодер.

Вы спросили, в чем разница, но более полезный вопрос: когда следует использовать шаблон спецификации?

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

Во-первых, вы должны понимать, что это не общая теория, это всего лишь конкретная модель; с конкретным моделированием классов {Specification, AndSpecification, ...}. Имея в виду более широкую теорию, управляемую доменом, вы можете отказаться от этого шаблона и при этом иметь превосходные варианты, с которыми все знакомы: например, хорошо названные объекты / методы / свойства для моделирования языка и логики домена.

Джеффри сказал:

Объект спецификации - это просто предикат, заключенный в объект

Это верно для домена, но не для Спецификационного шаблона. Джеффри подробно описывает ситуацию, в которой может возникнуть необходимость динамически создать выражение IQueryable, чтобы оно могло эффективно выполняться в хранилище данных (база данных SQL). Его окончательный вывод заключается в том, что вы не можете сделать это с помощью шаблона спецификации, как это предписано. Деревья выражений Джеффри IQueryable - это один из альтернативных способов изолировать логические правила и применять их в разных композитах. Как вы можете видеть из его примера кода, работать с ним многословно и очень неудобно. Я не могу представить себе ситуацию, которая потребовала бы таких динамических композитов. И при необходимости, есть много других доступных методов, которые проще.

Мы все знаем, что вам следует оптимизировать производительность в последнюю очередь. Попытка достичь Bleeding edge с деревьями выражений IQueryable - это ловушка. Вместо этого начните с лучших инструментов, простого и краткого Property Getter. Затем проверьте, оцените и определите приоритеты оставшейся работы.

Мне еще предстоит столкнуться с ситуацией, когда этот шаблон спецификации необходим / лучше. Когда я сталкиваюсь с предполагаемыми ситуациями, я перечислю их здесь и опровергну их. Если я столкнусь с хорошей ситуацией, я пересмотрю этот ответ с новым разделом.

RE: ответ zerkms

Потому что с помощью класса спецификации вы можете создавать новые критерии [sic] без модификации самих объектов.

C # уже обслуживает такие ситуации:

  • Наследование (в общем), где вы затем расширяете унаследованный класс (это хорошо, если вы не владеете пространством имен / библиотекой, откуда класс приходит)
  • Переопределение метода в наследовании
  • Частично - отлично, когда у вас есть классы моделей данных. Вы можете добавить свойства [NotStored] и наслаждаться всеми благами доступа к нужной вам информации непосредственно с объекта. Когда вы нажимаете «.» IntelliSense сообщает вам, какие члены доступны.
  • Методы расширения хороши, когда наследование нецелесообразно (архитектура не поддерживает его) или если родительский класс запечатан.

В проектах, от которых я вступаю во владение, я сталкиваюсь с анти-шаблонами, такими как Specification Pattern и другими. Они часто находятся в отдельном проекте / библиотеке (чрезмерная фрагментация проектов - еще одна ужасная практика), и все слишком напуганы, чтобы расширять объекты.

3 голосов
/ 15 декабря 2010

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

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

...