C# 中的规范模式






4.72/5 (62投票s)
本文将介绍如何在 C# 中使用规范模式组合 LINQ 查询。
引言
在本文中,我们将探讨经典的规范模式,并使用它来组合 LINQ 查询以及非 LINQ 查询。本文还将帮助初学者理解规范模式及其在实践中的实现方法。规范模式的主要优点包括可重用性、可维护性、将业务规则与业务对象松散耦合、可读性以及易于测试。
规范模式
根据维基百科的定义,规范模式是一种特定的软件设计模式,其中业务规则可以通过布尔逻辑将业务规则链接在一起进行重组。 简单来说,业务规则根据单一职责原则 (SRP) 进行分离,并通过布尔运算符(AND、OR 或 NOT)进行链接或组合以达到所需的结果。每个分离的业务规则称为一个规范。
每个规范都继承 `abstract` `CompositeSpecification` 类,该类定义了一个名为 `IsSatisfiedBy` 的 `abstract` 方法。此方法是规范模式的引擎。此方法负责将业务规则(规范)应用于有问题的对象并返回布尔结果。规范模式的主要目标是选择满足一组链接在一起的规范的对象子集。
ISpecification<T>
接口 `ISpecificaton<T>` 是规范定义所在的地方。每个规范都应该实现四个方法:`IsSatisfiedby()`、`And()`、`Or()` 和 `Not()`。如前所述,`IsSatisfiedBy` 方法负责将业务规则应用于对象。`And`、`Or` 和 `Not` 布尔运算符方法有助于将规范链接在一起以创建复合业务规则。规范模式不限于这三个布尔运算符(即,还可以根据需要有 `AndOr`、`AndNot` 等方法,但链接 `And` 和 `Or` 会得到 `AndOr`,因此这些方法的存在仅为方便起见)。此外,如果您能想到,也可以自由发挥定义自己的链接方法。
public interface ISpecification<T> {
bool IsSatisfiedBy(T o);
ISpecification<T> And(ISpecification<T> specification);
ISpecification<T> Or(ISpecification<T> specification);
ISpecification<T> Not(ISpecification<T> specification);
}
CompositeSpecification 类
`And`、`OR` 和 `Not` 方法的实现对于所有规范都是相同的,只有 `IsSatisfiedBy` 会根据业务规则而变化。因此,我们定义了一个名为 `CompositeSpecification` 的 `abstract` 类,它实现了 `And()`、`Or()` 和 `Not()` 方法,并将 `IsSatisfiedBy` 方法声明为 `abstract`,留给子类实现。
下面可以看到接口和 `abstract` 类的实际代码
public abstract class CompositeSpecification<T> : ISpecification<T>
{
public abstract bool IsSatisfiedBy(T o);
public ISpecification<T> And(ISpecification<T> specification)
{
return new AndSpecification<T>(this, specification);
}
public ISpecification<T> Or(ISpecification<T> specification)
{
return new OrSpecification<T>(this, specification);
}
public ISpecification<T> Not(ISpecification<T> specification)
{
return new NotSpecification<T>(specification);
}
}
`And`、`Or` 和 `Not` 方法分别创建并返回 `AndSpecification`、`OrSpecification` 和 `NotSpecification` 对象。现在不用担心这些类,后面章节会讨论。如果您仔细观察,会发现 `AndSpecification` 和 `OrSpecification` 类接受两个 `ISpecification` 参数,而 `Not` 只接受一个参数,这是因为前者是二元运算符,后者是一元运算符。例如,假设我们需要筛选品牌为 Samsung 且类型为 Smart phone 的手机。我们为品牌(检查 Samsung)和类型(检查 smart)分别定义两个规范。结果的手机对象应该同时满足这两个规范。为了实现这一点,我们使用 `And` 方法链接品牌和类型规范。类似地,要选择品牌为 Samsung 或类型为 smart 的手机,我们将两个规范与 `Or` 方法链接起来。在这两种情况下,我们都使用了两个规范。但是,要选择 Samsung 以外的所有品牌,我们只需要一个规范(品牌),这就是 `Not` 方法的情况。
另外,请注意,`And` 和 `Or` 方法将 `this` 关键字用作第一个参数。下面的示例可以更好地说明这一点。
ISpecification SamsungSmartSpec = samsungBrandSpec.And(smartTypeSpec);
`And` 方法本身是从另一个规范调用的,因此我们利用这一点,将调用规范作为创建 `AndSpecification` 的参数之一。这样做比这样做更具可读性。
ISpecification SamsungSmartSpec = dummySpecification.And(samsungBrandSpec, smartTypeSpec);
链接规范
这些规范类主要用于链接目的。这些类本身不封装任何业务规则,而是帮助具有业务规则的类进行链接。下面展示了定义“`And`”布尔运算符的类之一的代码。
public class AndSpecification<T> : CompositeSpecification<T>
{
ISpecification<T> leftSpecification;
ISpecification<T> rightSpecification;
public AndSpecification(ISpecification<T> left, ISpecification<T> right) {
this.leftSpecification = left;
this.rightSpecification = right;
}
public override bool IsSatisfiedBy(T o) {
return this.leftSpecification.IsSatisfiedBy(o)
&& this.rightSpecification.IsSatisfiedBy(o);
}
}
正如您所见,`IsSatisfiedBy` 方法会调用其他规范(包含业务规则)上的相应方法,并使用布尔 `AND` (&&) 组合结果。布尔 `AND` 的操作数,即左和右规范,在对象实例化期间(参见构造函数)由 `compositeSpecification` 类分配,如上一节所述。`And` 和 `Not` 规范类也以类似的方式实现。由于长度限制,此处未提供这些类的代码,但完整的源代码将在本文末尾附加供您下载。
表达式规范
我们已接近完成,但尚未完全结束。我们已经了解了规范的定义以及如何将它们链接在一起。但是,实际包含业务规则的规范又如何呢?在此处……
由于我们专注于基于 LINQ 的表达式,因此定义一个包含 LINQ 表达式的规范就足够了。可以检查对象上的条件并返回布尔结果的 LINQ 查询可以表示为 `Func<T, bool>` 类型。因此,我们的规范类接受此类类型的参数。`IsSatisfiedBy` 方法在接收到的对象上执行此 LINQ 表达式,并返回 true 或 false 结果。**除非没有链接,否则此方法由链接规范类((AndSpecification、OrSpecification 或 NotSpecification))调用。**
public class ExpressionSpecification<T> : CompositeSpecification<T> {
private Func<T, bool> expression;
public ExpressionSpecification(Func<T, bool> expression) {
if (expression == null)
throw new ArgumentNullException();
else
this.expression = expression;
}
public override bool IsSatisfiedBy(T o) {
return this.expression(o);
}
}
实际用法
最后,让我们通过一个简单的例子来看看实际用法。在我们的例子中,我创建了一个 Mobile phone 对象的列表,每个对象都有特定的品牌和类型(这些类的源代码可以在本文末尾附加的文件中找到)。我们创建规范以根据类型或品牌选择特定类型的手机型号。我们还通过链接简单规范来创建复杂/复合规范。
class Program
{
static void Main(string[] args)
{
List<Mobile> mobiles = new List<Mobile> {
new Mobile(BrandName.Samsung, Type.Smart, 700),
new Mobile(BrandName.Apple, Type.Smart),
new Mobile(BrandName.Htc, Type.Basic),
new Mobile(BrandName.Samsung, Type.Basic) };
ISpecification<Mobile> samsungExpSpec =
new ExpressionSpecification<Mobile>(o => o.BrandName == BrandName.Samsung);
ISpecification<Mobile> htcExpSpec =
new ExpressionSpecification<Mobile>(o => o.BrandName == BrandName.Htc);
ISpecification<Mobile> SamsungHtcExpSpec = samsungExpSpec.Or(htcExpSpec);
ISpecification<Mobile> NoSamsungExpSpec =
new ExpressionSpecification<Mobile>(o => o.BrandName != BrandName.Samsung);
var samsungMobiles = mobiles.FindAll(o => samsungExpSpec.IsStatisfiedBy(o));
var htcMobiles = mobiles.FindAll(o => htcExpSpec.IsStatisfiedBy(o));
var samsungHtcMobiles = mobiles.FindAll(o => SamsungHtcExpSpec.IsStatisfiedBy(o));
var noSamsungMobiles = mobiles.FindAll(o => NoSamsungExpSpec.IsStatisfiedBy(o));
}
}
在这里,我们定义了四种不同的规范,如下所示:
- 仅限 Samsung 品牌
- 仅限 HTC 品牌
- Samsung 或 HTC 品牌之一
- 除 Samsung 品牌外
前两个规范很简单;使用 LINQ 条件创建了一个简单的表达式规范。对于第三条规则,通过使用 `And` 方法链接为 1 和 2 定义的规范来创建复合规范。第四条规范也通过使用 `Not` 来实现,非常简单。
类似地,我们可以通过链接简单规范或另一个复杂规范来定义更复杂的规范。例如,要查找品牌为 Samsung 或 HTC 但仅类型为 smart 的手机,可以按如下方式组合查询:
ISpecification<Mobile> complexSpec = (samsungExpSpec.Or(htcExpSpec)).And(brandExpSpec);
非 LINQ 表达式
在某些情况下,我们可能需要在不使用 LINQ 表达式的情况下定义业务规则。在这种情况下,`expressionSpecification` 类被数量众多的简单但不同的规范类所取代,其数量等于所涉及的业务规则的数量。在上面的示例中,要查找品牌为 Samsung 或 HTC 的手机,我们需要两个不同的规范类,一个用于 Samsung 品牌检查,另一个用于 HTC 品牌检查。还要注意,这些规范是类型特定的,换句话说,与对象模型紧密耦合。例如,要检查手机品牌是否为 Samsung,规范类需要提前知道对象的类型。下面的示例是一个非 LINQ 规范,用于根据预定义成本评估手机是否为高端手机。
public class PremiumSpecification<T> : CompositeSpecification<T>
{
private int cost;
public PremiumSpecification(int cost) {
this.cost = cost;
}
public override bool IsSatisfiedBy(T o) {
return (o as Mobile).Cost >= this.cost;
}
}
正如您所看到的,`IsSatisfiedBy` 方法需要提前知道对象的类型(在本例中,对象类型为 `Mobile`)才能访问其 `Cost` 属性。
混合使用
在极少数情况下,我们可能需要在业务逻辑中混合使用 LINQ 和非 LINQ 表达式。由于链接规范类(`And`、`Or` 和 `Not` 规范)与业务规则规范类(`expressionSpecification` 和非 LINQ 规范)是分开的,因此我们可以将非 LINQ 与 LINQ 表达式类型规范链接起来。在上一节中,我们定义了一个非 LINQ 规范。现在我们可以将其与 LINQ 表达式链接,如下所示:
ISpecification<Mobile> premiumSpecification = new PremiumSpecification<Mobile>(600);
ISpecification<Mobile> linqNonLinqExpSpec = NoSamsungExpSpec.And(premiumSpecification);
这将获取所有非 Samsung 品牌的手机,但仅限高端手机。在我们的类中,结果是 Apple。
一个用例
非常感谢
我要感谢所有撰写了以下文章的人,他们的文章帮助我提升了对这个主题的知识,没有他们,这篇文章就不会成为现实。