14

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

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

new CustomerHasEnoughMoneyInTheirAccount().IsSatisfiedBy(customer)

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

public class Customer
{

     public double Balance;

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

从客户端代码:

customer.HasEnoughMoney

所以我的问题真的是;使用属性 getter 包装业务逻辑和创建规范类有什么区别?

谢谢大家!

4

4 回答 4

11

是的,这是没有意义的。

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

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

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

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

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

杰弗里说:

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

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

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

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

回复:zerkms 回答

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

C# 已经满足了这样的情况:

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

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

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

于 2016-09-30T07:06:01.493 回答
10

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

于 2010-12-15T02:33:02.653 回答
10

在一般意义上,规范对象只是包裹在对象中的谓词。如果谓词非常常与类一起使用,则将谓词移动到它适用的类中可能是有意义的。

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

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用非常简单的静态构建器方法来实现规范。

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- 请注意,如果您只有一个简单的委托,您根本无法看到表达式的结构!一些注意事项:aDisplayClass是编译器生成的保存在闭包中捕获的局部变量的类,以处理向上的 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))))

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

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

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

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

于 2010-12-15T02:42:22.790 回答
3

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

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

于 2010-12-15T02:44:52.790 回答