【问题标题】:Is Specification Pattern Pointless?规范模式毫无意义吗?
【发布时间】:2010-12-15 02:27:44
【问题描述】:

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

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

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 类有什么区别?

提前谢谢大家!

【问题讨论】:

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


    【解决方案1】:

    在一般意义上,规范对象只是包裹在对象中的谓词。如果一个谓词非常常用于一个类,那么将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");
    

    我一直在玩一些实验性代码,这些代码根据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;
            }
        }
    }
    

    也就是说,这是一整套没有增加价值的样板!这些Expressions 只查看公共属性,因此可以轻松地使用普通的旧 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;
            }
        }
    }
    

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

    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# 的典型 @ 987654341@.)

    _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 嵌套的 lambda(我还将 α-converted 所有参数名称替换为唯一生成的符号,作为简化 β-reduction 的中间步骤),一个更简单的Expression 树结果:

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

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

    顺便说一句,我建立了一个愚蠢的小基准测试,实际上发现闭包引用消除对示例Expression 编译到委托时的评估速度有显着的性能影响 - 它缩短了评估时间几乎一半(!),在我碰巧坐在前面的机器上,每次调用从 134.1ns 到 70.5ns。另一方面,β-减少没有可察觉的差异,也许是因为编译无论如何都会这样做。在任何情况下,我都怀疑传统的规范类集能否在四个条件的组合下达到那种评估速度;如果出于其他原因(例如 builder-UI 代码的便利性)必须构建这样的常规类集,我认为最好让类集生成 Expression 而不是直接评估,但首先考虑您是否需要C# 中的模式 - 我见过太多规范过量的代码。

    【讨论】:

    • 从你所说的,我想说你可以通过为使用规范类的客户类创建扩展方法来获得这两种方法的好处
    • @Earlz 确实如此。但是,如果您这样做,我认为Expression 失去了一些力量——如果它只包含一个MethodInfo 命名扩展方法而不是实际条件,它就不能通过底层IQueryable提供者执行远程过滤或索引优化。
    • 在哪些情况下需要序列化这样的规范?为什么要拐弯抹角? var spec = Customer.HasFunds &amp;&amp; Customer.AccountAgedAtLeast(TimeSpan.FromDays(180)) &amp;&amp; Customer.LocatedInState("NY")如果需要序列化,为什么不直接序列化参数呢?
    • @Todd,IQueryable 的典型 ORM 实现是一个动态 SQL 工厂。鉴于Expression 没有方法引用(如我的回答),大多数都能够将Expression 转换为WHERE 子句元素。另一方面,按名称提及任何域方法通常会导致那些从WHERE 和过滤客户端被忽略,因为方法 实现 不是生成的Expression 树的一部分 - 只有一个MethodCallExpression 节点包含一个 MethodInfo 描述要调用的函数。
    • @JeffreyHantin 您对 ORM 是正确的,但 OP 没有提到这一点。规范模式(这是主题)不是为与 ORM 一起使用而设计的。话虽如此,仍然可以在 EF 模型类上使用函数和属性,您只需要在以下 IEnumerable 集或迭代器中执行此操作。 var dbAsUsual = (from c in customers {filtering, grouping, ordering, select projection}); 然后var finalSet = (from c in dbAsUsual.AsEnumerable() where c.CustomerHasFunds &amp;&amp; c.AccountAgeAtLeast(TimeSpan.FromDays(180)) &amp;&amp; c.LocatedInState("NY"));
    【解决方案2】:

    是的,没有意义。

    The Wikipedia article 详细批评了这种模式。但我认为最大的批评仅仅是Inner-Platform Effect。为什么要重新发明 AND 运算符?请务必阅读 Wikipedia 文章以获取完整图片。

    Henry,您认为 Property Get 更胜一筹是正确的。为什么要避开一个更简单、易于理解的 OO 概念,因为一个模糊的“模式”在其概念中并不能回答您的问题?这是一个想法,但很糟糕。这是一种反模式,一种对你这个编码员不利的模式。

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

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

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

    杰弗里说:

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

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

    我们都知道您应该最后优化性能。在这里尝试使用 IQueryable 表达式树实现Bleeding edge 是一个陷阱。相反,从最好的工具开始,首先是一个简单而简洁的 Property Getter。然后测试、评估并确定剩余工作的优先级。

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

    RE:zerkms 回答

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

    C# 已经满足了这种情况:

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

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

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

      • objects?你的意思是class 对吗?这不是面向对象的高内聚和紧密耦合的重点吗?如果您对 Customer 类具有主要访问权限,则它应该是一个属性。如果它链接到代码优先类,则可以使用部分扩展。根据我的经验,规范模式是一种反模式。
      【解决方案4】:

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

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

      【讨论】:

        猜你喜欢
        • 2010-09-11
        • 2014-12-27
        • 1970-01-01
        • 1970-01-01
        • 2023-04-01
        • 1970-01-01
        • 1970-01-01
        • 2011-04-18
        • 1970-01-01
        相关资源
        最近更新 更多