规格模式没有意义吗?

时间:2010-12-15 02:27:45

标签: c# domain-driven-design specification-pattern

我只是想知道规范模式是否毫无意义,给出以下示例:

假设您要检查客户是否在其帐户中有足够的余额,您可以创建类似以下内容的规范:

new CustomerHasEnoughMoneyInTheirAccount().IsSatisfiedBy(customer)

但是,我想知道的是,我可以通过在Customer类中使用Property getter来实现规范模式的相同“好处”(例如只需要在适当的位置更改业务规则):

public class Customer
{

     public double Balance;

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

来自客户代码:

customer.HasEnoughMoney

所以我的问题确实是;使用属性getter来包装业务逻辑和创建Specification类之间的区别是什么?

提前谢谢大家!

4 个答案:

答案 0 :(得分:9)

在一般意义上,Specification对象只是一个包含在对象中的谓词。如果一个谓词非常常用于一个类,那么将Move Method谓词放入它所适用的类中可能是有意义的。

当你构建更复杂的东西时,这种模式真正发挥作用:

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

传递或序列化;当你提供某种“规范构建器”UI时,它会更有意义。

那就是说,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只看公共属性,所以人们可以很容易地使用普通的拉姆达!现在,如果其中一个规范需要访问非公共状态,我们真的需要一个可以访问非公共状态的构建器方法。我将在此处使用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是编译器生成的类,它保存在闭包中捕获的局部变量,以处理upwards funarg problem;转储的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))))

凌乱!大量调用立即lambda和保留对构建器方法中创建的闭包的引用。通过将闭包引用替换为其捕获的值并将β-reducing嵌套的lambdas(我也将α-converted所有参数名称替换为唯一生成的符号作为简化β减少的中间步骤),更简单{{1}树结果:

Expression

然后可以进一步组合这些_0 => ((_0.AvailableFunds >= 500.00) && ((_0.AccountOpenDateTime <= (DateTime.UtcNow - 180.00:00:00)) && ((_0.Address.State = "NY") && (_0.lastCreditScore >= 667)))) 树,编译成委托,漂亮打印,编辑,传递给理解Expression树的LINQ接口(例如EF提供的树),或者有什么你。

另一方面,我构建了一个愚蠢的小微基准,并且实际上发现,当编译给代表时,关闭引用消除对示例Expression的评估速度具有显着的性能影响 - 它切断了评估时间差不多一半(!),从机器上每次通话134.1ns到70.5ns我碰巧坐在前面。另一方面,β减少没有发现可检测到的差异,也许是因为编译无论如何都会这样做。在任何情况下,我都怀疑传统的规范类集可以达到四种条件组合的评估速度;如果由于其他原因(例如构建器-UI代码的便利性)而必须构建这样的传统类集,我认为让类集产生Expression而不是直接评估是明智的,但首先要考虑是否你需要C#中的模式 - 我已经看过太多的规范 - 过量的代码。

答案 1 :(得分:8)

因为使用规范类,您可以创建新的标准而无需修改对象本身。

答案 2 :(得分:3)

参见zerkms的回答,另外:规范也可以用于接口等抽象类型或作为通用,使其适用于整个范围的对象。

或者需要对客户进行的检查可能取决于上下文。例如,客户对象可能对付费角色系统无效,但对于在用户再次登录时在进程中间将其保存在数据库中以进行进一步处理是有效的。通过规范,您可以在集中位置构建相关检查组,并根据上下文切换整个集合。在这种情况下,您可以将它与工厂模式结合起来。

答案 3 :(得分:3)

是的,这是没有意义的。

The Wikipedia article详细批评了这种模式。但我认为最大的批评仅仅是Inner-Platform Effect。为什么重新发明AND运算符?再次阅读维基百科文章以获得更全面的批评。

Henry,你认为Property Get是优越的是正确的。为什么要避免使用一个更简单,易于理解的OO概念,因为一个模糊的“模式”在其概念中并没有回答你的问题?这是一个想法,但不是一个坏主意。这是一种反模式,一种对你不利的模式,编码器。

您已经问过有什么区别,但更有用的问题是,何时应该使用规范模式?

永远不要使用此模式,这是此模式的一般规则。

首先,你应该意识到这不是一般理论,它只是一种特定的模式;使用类{Specification,AndSpecification,...}的特定建模。考虑到更广泛的领域驱动理论,您可以放弃这种模式,并且仍然拥有每个人都熟悉的优秀选项:例如,用于模拟领域语言和逻辑的名称良好的对象/方法/属性。

杰弗里说:

  

规范对象只是一个包含在对象中的谓词

域驱动的情况确实如此,但具体不是规范模式。 Jeffrey全面描述了一种情况,人们可能希望动态构建IQueryable表达式,因此它可以在数据存储(SQL数据库)上高效执行。他的最后结论是,你不能按照规定使用规范模式。 Jeffrey的IQueryable表达式树是一种隔离逻辑规则并将其应用于不同组合的替代方法。正如您从他的示例代码中看到的那样,使用起来很冗长且非常尴尬。我无法想象任何需要这种动态复合材料的情况。如果需要,还有许多其他技术更简单。

我们都知道你应该最后优化表现。尝试使用IQueryable表达式树来实现Bleeding edge是一个陷阱。相反,首先要使用最好的工具,一个简单而简洁的Property Getter。然后测试,评估并优先考虑剩下的工作。

我还没有遇到这种规范模式是必要/更好的情况。当我遇到假设的情况时,我会在这里列出并反驳它们。如果我遇到一个好的情况,我会用新的部分修改这个答案。

RE:zerkms回答

  

因为使用规范类,您可以创建新的标准[原文如此]   没有修改对象本身。

C#已经迎合了这种情况:

  • 继承(一般情况下),然后扩展继承的类(当你没有拥有类所在的命名空间/库时,这很好)
  • 方法覆盖继承
  • 部分 - 有数据模型类时很棒。您可以同时添加[NotStored]属性,并享受直接从对象访问所需信息的所有乐趣。当你按“。” IntelliSense会告诉您哪些成员可用。
  • 如果继承不实用(架构不支持),或父类被密封,扩展方法很棒。

在我接手的项目中,我遇到了像规范模式等反模式。他们经常在一个单独的项目/图书馆(项目的过度分割是另一种可怕的做法),每个人都害怕扩展对象。