重新实现 Specification 模式
技术分享
Dec 30, 2023
type
Post
status
Published
date
Dec 30, 2023
slug
reimplement-specification
summary
本文介绍 Specification 模式并使用函数式编程和 TDD 的方法重新实现该模式
tags
Design Pattern
Java
TDD
category
技术分享
icon
password
Property
Jan 5, 2024 05:58 AM

什么是 Specification 模式

设计模式是各位软件大师及前辈们的智慧结晶,他们对软件开发中的各种问题进行深入的思考,通过抽象、归纳等手法提炼出可重用的解决方案,本文所述的 Specification 模式就是一种设计模式,它是《领域驱动设计》的作者 Eric Evans 与软件大师 Martin Fowler 协作开发出来的,原始论文在这里
如果从 DDD 的角度来说,Specification 被定义为提供了一种用来表达业务领域模型中特定类型规则的精确方式,它把这些规则从条件逻辑中提取出来,在模型中把它们显式地表示出来,并将规则保留在领域层中,更加内聚且易于使用这些规则,本文不会过多的阐述和讨论 DDD 的 Specification 模式,我们所讲的是一种更加普遍适用的设计模式。
如果跟 GoF 的那些设计模式相比,Specification 模式算是有点特殊的设计模式,主要用于描述业务规则并提供了一种有效的方式来判断对象是否满足指定的规则,它是一种实现业务逻辑和对象模型分离的有效方法,特别适用于那些业务规则复杂的场景,例如需要对一组对象进行筛选,筛选的规则可能非常复杂,而且经常需要改变。
在原始的 Specification 设计模式中,每个业务规则都被封装为一个具有单一职责的具体 Specification 对象。每个 Specification 对象都有一个 isSatisfiedBy 的方法,这个方法接受一个候选对象作为参数,判断这个候选对象是否满足 Specification 所描述的规则,本文的实现则不会拘泥于这些约定,使用更加自然和高效的方式来实现。
notion image
用一个实例来说明 Specification 模式的概念,比如你在开发一个酒店预订系统,酒店客人有一个细分群体,叫 VIP 客户,一个 VIP 客户可以被定义为"预订次数超过 5 次"或者"预订总费用超过100000 元"的客人,如上图所示。我们可以将这两个规则封装成两个独立的 Specification,然后利用 Specification 模式将它们组合成一个新的 Specification,这样在任何时候我们需要检查一个客人是否是 VIP 的时候,只需要复用这个组合的 Specification 即可,如果需要对此“VIP客人”的约束增加条件,如“并且实付金额超过50000元”,也只需要修改组合 Specification 的定义,而无需更改其他。

Specification模式的主要优点

高度可复用
Specification 模式的主要优点之一是高度可复用。通过将业务规则抽象为独立的 Specification,我们可以在任何需要使用这些规则的地方复用 Specification 对象,从而避免了业务规则的重复,提高了代码的可维护性和稳定性。
强大的组合能力
Specification 对象可以通过逻辑运算(如 andornot 等)进行组合,形成更复杂的业务规则,这使得我们可以使用一种声明式的方式来描述业务规则,为我们提供了一种强大的业务建模工具,我们可以轻松应对业务规则复杂度的增长,而无需过多修改现有的代码。
易于提升代码质量
通过使用 Specification 模式,我们可以将业务逻辑与代码实现分离,提高代码的可读性和可维护性,此外,由于每个 Specification 都是一个自我完备、可独立测试的单元,可以降低错误的可能性,提高代码质量

典型的使用场景

Specification 模式在各种场景都有可能应用,但它特别适用于下面两类情况
条件检查
在进行条件检查时,我们往往需要同时满足或不满足多个约束才能决定我们采取的业务处理逻辑,使用 Specification 模式,我们可以将每个约束条件封装为一个 Specification,然后通过组合这些Specification 来形成更为复杂的条件检查,这样我们能够得到一个规则清晰、易于理解的实现,并且可以避免出现大量重复的条件检查代码,进而避免散弹式修改。
数据过滤
在数据过滤中,我们经常需要根据一组复杂的条件去过滤数据,得到我们期望的结果,我们可以将这些过滤条件抽象为 Specification,然后将 Specification 应用于数据集进行过滤,这样做的好处是过滤条件可以复用,而且我们可以通过组合复用这些 Specification,从而实现更复杂的过滤条件。

新的实现方式

notion image
由于在实际项目中需要使用这种模式,我便尝试使用全新的方法来重新实现 Specification 模式,以利用更新的语言特性带来的便利性和灵活性。通过对这个模式的分析和思考,常见的 Specification 模式的使用流程如上图所示,我们可以看出它其实是一种纯函数式的规约检查,对此进行抽象得到的算式就是 ,通过对 的计算得到一个布尔型的结果,十分适合采用函数式编程范式来实现。
还有就是此类工具和框架性质的任务很适合使用 TDD 的方法来进行编码,它能够保证我们对前期的 API 和框架的整体架构有个初始的设想就能顺利进行下去,而且随着实现的过程,可以十分安全的重构成最终理想的形态。
TDD 的要义和难点就是任务拆分,也就是大家常说的 Tasking,针对这个问题,有个很好的切入点就是实际业务需求,我们可以先将需求切分为功能点,再将功能点细分为任务项,自然而然地就得到了我们所需颗粒度的测试点,下面是我对 Specification 模式的任务拆分结果
  • 可以构建针对某个对象的 Specification
    • 支持相等判断,即 eq 操作
    • 支持大于判断,即 gt 操作
    • 支持小于判断,即 lt 操作
    • 支持大于等于判断,即 ge 操作
    • 支持小于等于判断,即 le 操作
  • Specification 可以自由组合
    • 支持 and 操作符组合
    • 支持 or 操作符组合
    • 支持 not 操作符组合
    • 支持操作符之间的混合组合
  • Specification 可以实现对某个对象进行校验
  • Specification 可以实现对数据进行筛选
    • 支持在 Stream API 中使用
得到任务列表之后,还需要对用户使用的 API 进行初期的构思,然后就可以进行单元测试的编写,确定每个任务项的 Happy path 和 Sad path 都覆盖到,等到所有测试通过,整个 Specification 模式的实现也就完成了,最终得到的测试差不多就是下图中的样子
notion image

代码

整个实现的代码简单易懂,而且 API 也十分易用,基本实现了我们最初的目标,代码已经上传到 Github,如果感兴趣可以自行阅读
//使用示例 Predicate<Product> productPredicate = Specification.and(of(Product::name).eq("iPhone 15"), of(Product::owner).eq(ALICE), of(Product::quantity).le(10)); assertTrue(productPredicate.test(new Product(ALICE, "iPhone 15", 2, "Mobile Phone")));

总结

以上就是 Specification 模式的简单介绍以及如何使用 Java 中函数式编程的一点小技巧重新实现该模式,希望能给各位带来启发。
 
  • Design Pattern
  • Java
  • TDD
  • 解析分布式事务解决方案 - Saga 模式
    写作课习作汇总