В общем смысле объект спецификации - это просто предикат, заключенный в объект.Если предикат очень часто используется с классом, может иметь смысл 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 # - я видел слишком много перегруженного спецификациями кода.