规格模式没有意义吗?

Hen*_*ung 10 c# domain-driven-design specification-pattern

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

假设您要检查客户是否在他/她的帐户中有足够的余额,您可以创建如下规范:

new CustomerHasEnoughMoneyInTheirAccount().IsSatisfiedBy(customer)
Run Code Online (Sandbox Code Playgroud)

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

public class Customer
{

     public double Balance;

     public bool HasEnoughMoney
     {
          get { return this.Balance > 0; }
     }
}
Run Code Online (Sandbox Code Playgroud)

来自客户代码:

customer.HasEnoughMoney
Run Code Online (Sandbox Code Playgroud)

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

谢谢大家!

Jef*_*tin 9

在一般意义上,Specification对象只是一个包含在对象中的谓词.如果谓词非常常用于类,那么谓词移动到它应用的类中可能是有意义的.

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

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

并将其传递或序列化; 当你提供某种"规范构建器"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");
Run Code Online (Sandbox Code Playgroud)

我一直在玩一些实验代码,用Expressions语言实现规范,使用非常简单的静态构建器方法.

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

也就是说,这是一整套没有增加价值的样板! 这些Expression只关注公共财产,所以人们可以轻松地使用普通的老lambda!现在,如果这些产品规格的一个需要访问非公开状态,我们真的需要访问非公开状态的建设者方法.我将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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我们还需要一种方法来组合这些规范 - 在这种情况下,需要所有子项都是真的复合:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我想这部分的缺点是它可能导致复杂的Expression树木.例如,构建这个:

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

产生一个Expression看起来像这样的树.(这些是ToString()在调用时返回的格式的稍微格式化的版本Expression- 请注意,如果您只有一个简单的委托,则根本无法看到表达式的结构!几个注释:a DisplayClass是编译器生成的保存在闭包中捕获的局部变量的类,用于处理向上的funarg问题 ;而dumped 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))))
Run Code Online (Sandbox Code Playgroud)

乱!大量调用立即lambda和保留对构建器方法中创建的闭包的引用.通过用它们捕获的值替换闭包引用并且减少嵌套的lambda(我还将所有参数名称α转换为唯一生成的符号作为简化β缩减的中间步骤),得到一个更简单的Expression树:

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

Expression然后可以将这些树进一步组合,编译成代理,漂亮打印,编辑,传递给理解Expression树的LINQ接口(例如EF提供的那些),或者你有什么.

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


zer*_*kms 8

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


Tod*_*odd 8

是的,这是毫无意义的。

维基百科文章详细批评了这种模式。但我认为最大的批评仅仅是内部平台效应。为什么要重新发明 AND 运算符?请务必阅读维基百科文章以获取完整图片。

亨利,您认为 Property Get 更胜一筹是正确的。为什么要避开一个更简单、易于理解的 OO 概念,因为它的概念中没有回答您的问题的晦涩“模式”?这是一个想法,但很糟糕。这是一种反模式,一种对您(编码人员)不利的模式。

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

永远不要使用这种模式是我对这种模式的一般规则。

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

杰弗里说:

规范对象只是包裹在对象中的谓词

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

我们都知道你应该最后优化性能。试图在这里实现出血边缘使用 IQueryable 表达式树是一个陷阱。相反,首先从最好的工具开始,一个简单而简洁的 Property Getter。然后测试、评估和优先考虑剩下的工作。

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

RE:zerkms 的回答

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

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

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

这些都是全球传授的想法,大多数程序员已经自然地理解和使用了。

在我接手的项目中,我确实遇到过规范模式等反模式。它们通常位于单独的项目/库中(项目的过度碎片化是另一种可怕的做法),并且每个人都不敢扩展对象。