规范模式毫无意义吗?

发布于 2024-10-07 19:44:50 字数 570 浏览 0 评论 0原文

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

假设您想检查客户的帐户中是否有足够的余额,您将创建一个类似于以下的规范:

new CustomerHasEnoughMoneyInTheirAccount().IsSatisfiedBy(customer)

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

public class Customer
{

     public double Balance;

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

从客户端代码:

customer.HasEnoughMoney

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

提前谢谢大家!

I'm just wondering if Specification pattern is pointless, given following example:

Say you want to check if a Customer has enough balance in his/her account, you would create a specification something like:

new CustomerHasEnoughMoneyInTheirAccount().IsSatisfiedBy(customer)

However, what I'm wondering is I can achieve the same "benefits" of Specification pattern (such as only needing to change the business rules in on place) by using Property getter in the Customer class like this:

public class Customer
{

     public double Balance;

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

From client code:

customer.HasEnoughMoney

So my question really is; what is the difference between using the property getter to wrap the business logic, and creating Specification class?

Thank you all in advance!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

你在看孤独的风景 2024-10-14 19:44:50

是的,这是毫无意义的。

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

亨利,你认为“财产获取”更优越是正确的。为什么要避开一个更简单、易于理解的 OO 概念,而选择一个晦涩的“模式”,而它的概念并不能回答您的问题呢?这是一个想法,但却是一个糟糕的想法。这是一种反模式,一种对你(编码员)不利的模式。

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

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

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

杰弗里说:

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

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

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

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

回复:zerkms 的回答

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

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

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

这些是全球范围内传授的思想,大多数程序员已经自然地理解和使用。

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

回复:杰弗里·汉顿

请参阅https://stackoverflow.com/a/4446254/887092 [ 2023-01-23]

在许多情况下,使用 Expression 可能非常合适。我仍然相信更简单的代码也能正常工作。它可能会有点混乱,但我相信简单性是第一位的。不要让新员工感到困惑,否则你就锁定了需要聘请专家的公司,而这些专家通常在扩展方面供不应求。

这是他的规范模式示例:

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

使用纯代码,它就变成了

结果 = (CustomerSpec.HasFunds(500.0m) &&
CustomerSpec.AccountAgeAtLeast(TimeSpan.FromDays(180)) &&
CustomerSpec.LocationInState);

如果您需要复杂的东西,例如多个 &&和一些()隔离||。这就是您在计算机科学 101 中学到的内容。它在所有编程语言中也是相同的(语法除外)。

Yes, it is pointless.

The Wikipedia article criticises this pattern at length. But I see the biggest criticism being solely the Inner-Platform Effect. Why re-invent the AND operator? Please be sure to read the Wikipedia article for the complete picture.

Henry, you are correct to assume the Property Get is superior. Why eschew a simpler, well-understood OO concept, for an obscure "pattern" which in its conception doesn't answer your very question? It's an idea, but a bad one. It's an anti-pattern, a pattern that works against you, the coder.

You have asked what is the difference, but a more useful question is, when should a Specification Pattern be used?

Never use this pattern, is my general rule for this pattern.

First, you should realise this pattern isn't based on a scientific theory, it's only an arbitrary pattern someone imagined that uses a particular modeling of classes { Specification, AndSpecification, ...}. With the broader domain-driven theory in mind, you can abandon this pattern, and still have superior options that everyone is familiar with: for instance, well-named objects/methods/properties to model domain language and logic.

Jeffrey said:

a Specification object is just a predicate wrapped up in an object

That's true of domain-driven, but not the Specification Pattern specifically. Jeffrey, comprehensively describes a situation where one may want to dynamically build up an IQueryable expression, so it can efficiently execute on the data store (SQL Database). His final conclusion, is that you can't do that with the Specification Pattern as it's prescribed. Jeffrey's IQueryable expression trees are one alternative way to isolate logical rules and apply them in different composites. As you can see from his example code, it's verbose and very awkward to work with. I can't imagine any situation which would require such dynamic composites either. And if needed, there are many other techniques available which are simpler:-

We all know you should optimise performance last. Attempting here to achieve Bleeding edge with IQueryable expression trees, is a trap. Instead, start with the best tools, a simple and terse Property Getter first. Then test, evaluate and prioritise what work remains.

I am yet to experience a situation where this Specification Pattern is necessary/better. As I do come across supposed situations, I'll list them here and rebut them. If I come across a good situation, I'll revise this answer with a new section.

RE: zerkms answer

Because with the specification class you can create new criterias [sic]
without modification of the objects themselves.

C# already caters for such situations:

  • Inheritance (in General), where you then extend the inherited class (this is good when you don't own the namespace/library from whence the class comes)
  • Method Overriding in Inheritence
  • Partial - great when you have data-model classes. You can add [NotStored] properties alongside, and enjoy all the bliss of accessing the information you need directly off the object. When you press '.' IntelliSense tells you what members are available.
  • Extension Methods are great when Inheritance is not practical ( architecture doesn't support it ), or if the parent class is sealed.

And these are globally taught ideas that most programmers will already naturally understand and use.

In projects I take over from, I do encounter anti-patterns like Specification Pattern, and more. They're often in a separate Project/Library (over-fragmentation of Projects is another terrible practice) and everyone is too scared to extend objects.

RE: Jeffery Hanton

see https://stackoverflow.com/a/4446254/887092 [2023-01-23]

The use of Expression might be very suitable in many cases. I do still believe that simpler code will work just as well. There a chance it's a little messier, but I believe simplicity needs to come first. Don't confuse the new recruit, otherwise you lock the company needing to hire experts, which are normally in short supply for scaling.

This is his Specification Pattern example:

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

With plain code, it becomes

result = (CustomerSpec.HasFunds(500.0m) &&
CustomerSpec.AccountAgeAtLeast(TimeSpan.FromDays(180)) &&
CustomerSpec.LocatedInState);

If you ever need complexities, like multiple && and some () isolated ||. Then that's what you learn in Computer Science 101. It's also the same across all programming languages (except for syntax).

昔日梦未散 2024-10-14 19:44:50

一般来说,规范对象只是包装在对象中的谓词。如果谓词在类中非常常用,则移动方法可能有意义将谓词放入其适用的类中。

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

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;
        }
    }
}

也就是说,这是一整套不会增加价值的样板文件!这些表达式仅查看公共属性,因此人们可以轻松地使用普通的旧表达式拉姆达!现在,如果这些规范之一需要访问非公共状态,我们确实需要一个能够访问非公共状态的构建器方法。我将在这里使用 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))))

混乱!大量调用直接 lambda 并保留对构建器方法中创建的闭包的引用。通过用捕获的值替换闭包引用并β-reducing嵌套 lambda(我还α-转换所有参数名称为唯一生成的符号作为简化 β-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.1 纳秒到 70.5 纳秒。另一方面,β-reduction 没有产生可检测到的差异,也许是因为编译无论如何都会这样做。无论如何,我怀疑传统的规范类集能否达到四种条件组合的评估速度;如果出于其他原因(例如构建器 UI 代码的便利性)必须构建这样的常规类集,我认为建议让类集生成 Expression 而不是直接求值,但是首先考虑在 C# 中是否需要该模式 - 我见过太多规范过度的代码。

In the general sense, a Specification object is just a predicate wrapped up in an object. If a predicate is very commonly used with a class, it might make sense to Move Method the predicate into the class it applies to.

This pattern really comes into its own when you're building up something more complicated like this:

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

and passing it around or serializing it; it can make even more sense when you're providing some sort of "specification builder" UI.

That said, C# provides more idiomatic ways to express these sorts of things, such as extension methods and 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");

I've been playing around with some experimental code that implements Specifications in terms of Expressions, with very simple static builder methods.

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;
        }
    }
}

That said, this is a whole load of boilerplate that doesn't add value! These Expressions only look at public properties, so one could just as easily use a plain old lambda! Now, if one of these Specifications needs to access non-public state, we really do need a builder method with access to non-public state. I'll use lastCreditScore as an example here.

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;
        }
    }
}

We also need a way to make a composite of these Specifications - in this case, a composite that requires all children to be true:

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);
    }
}

I guess part of the downside to this is it can result in complicated Expression trees. For example, constructing this:

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

produces an Expression tree that looks like this. (These are slightly formatted versions of what ToString() returns when called on the Expression - note that you wouldn't be able to see the structure of the expression at all if you had only a simple delegate! A couple of notes: a DisplayClass is a compiler-generated class that holds local variables captured in a closure, to deal with the upwards funarg problem; and the dumped Expression uses a single = sign to represent equality comparison, rather than C#'s typical ==.)

_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))))

Messy! Lots of invocation of immediate lambdas and retained references to the closures created in the builder methods. By substituting closure references with their captured values and β-reducing the nested lambdas (I also α-converted all parameter names to unique generated symbols as an intermediate step to simplify β-reduction), a much simpler Expression tree results:

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

These Expression trees can then be further combined, compiled into delegates, pretty-printed, edited, passed to LINQ interfaces that understand Expression trees (such as those provided by EF), or what have you.

On a side note, I built a silly little micro-benchmark and actually discovered that closure reference elimination had a remarkable performance impact on the speed of evaluation of the example Expression when compiled to a delegate - it cut the evaluation time nearly in half(!), from 134.1ns to 70.5ns per call on the machine I happen to be sitting in front of. On the other hand, β-reduction made no detectable difference, perhaps because compilation does that anyway. In any case, I doubt a conventional Specification class set could reach that kind of evaluation speed for a composite of four conditions; if such a conventional class set had to be built for other reasons such as the convenience of builder-UI code, I think it would be advisable to have the class set produce an Expression rather than directly evaluate, but first consider whether you need the pattern at all in C# - I've seen way too much Specification-overdosed code.

嘴硬脾气大 2024-10-14 19:44:50

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

Because with the specification class you can create new criterias without modification of the objects themselves.

猥琐帝 2024-10-14 19:44:50

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

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

See zerkms answer, plus: a specification can also work on abstract types like interfaces or as a generic making it applicable to a whole range of objects.

Or the check that needs to be done on customer might depend on the context. For example a customer object might not be valid for the pay role system yet, but valid for saving it in the database in the middle of a process for further processing when the user logs in again. With specifications you can build groups of related checks in a centralised location and switch out the entire set depending on context. In this situation you'd combine it with a factory pattern for example.

方圜几里 2024-10-14 19:44:50

我是一名 Java 程序员。在我的工作中,我从规范模式中受益匪浅。

对于我的工作,我们在网络上构建了一个在线磁盘。我们使用elasticsearch来存储数据。我们总是需要在文件夹下或所需范围内搜索文件等。

所以我使用规范模式来实现它。

我定义规范接口。每个规范都可以生成一个elasticsearch查询,多个规范可以通过must、mustNot、should组合成一个组合规范。

所以在我的代码中,最后像这样搜索

search(inType(Type.FILE).must(inScope(Scope.Personal).must(routeUnderAny(folder))));

使用这个模式,我有一个干净且可读的代码。

但话又说回来,如果我不使用elasticsearch,而是使用sql和jpa。

我可以使用 Jooq 或 querydsl 来更轻松地实现规范模式。

Jooq 或 querydsl 就像 C# 中的 LINQ。

所以我认为并不是规范模式没有用,而是LINQ可以帮助我们更容易的实现规范模式。

LINQ 本身就是一种规范模式。

I'm a Java programmer. In my work, I have many benefit from specification pattern.

For my work, we construct a online disk on web. We use elasticsearch to store data. We always need to search file under a folder, or in desired scope, and etc.

So I use specification pattern to achieve it.

I define Specification Interface. Every Specification can generate a elasticsearch query, and several specifications can be composed by must, mustNot, should into a composed specification.

So in my code, finally search like this

search(inType(Type.FILE).must(inScope(Scope.Personal).must(routeUnderAny(folder))));

Use this pattern, I have a clean and readable code.

But then again, if I not use elasticsearch, but use sql and jpa.

I can use Jooq or querydsl to achieve specification pattern easier.

Jooq or querydsl is like LINQ in C#.

So I think it is not specification pattern useless, but LINQ can help us easier to achieve specification pattern.

LINQ itself is a specification pattern.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文