| name | metaframe-specification-pattern |
|---|---|
| category | engineering |
| description | 规格模式 | Code & Engineering |
英文名: Specification Pattern
分类: 编码实践
作者: Eric Evans
复杂度: intermediate
成熟度: established
AI 相关: ❌ 否
将业务规则封装为可组合、可复用的对象,通过布尔逻辑组合来表达复杂的领域谓词
- 规格即领域对象:规格是一等领域概念——命名的业务规则(EligibleForLoyaltyReward、PastDueInvoice),明确、有文档且可发现,而非隐藏在匿名谓词中
- 可组合性:规格支持布尔组合(And、Or、Not),复杂规则通过组合原子规格来表达,而非编写嵌套布尔表达式,保持每条规则独立可读
- 双用规格:良好实现的规格既可用于内存中验证(IsSatisfiedBy(entity)),也可通过表达式树用于数据库查询生成(ToExpression() → IQueryable.Where(spec))
- 规则集中化:通过在恰好一个规格类中编码每条业务规则,规则的变更(资格阈值变化、新例外)自动传播到使用该规格的所有地方
- 将是什么与如何分离:规格将业务规则是什么与如何应用分离——相同的EligibleForDiscountSpec可以驱动领域验证、数据库过滤和UI渲染,而这些上下文之间互不了解
- 识别在代码库中重复或混入查询逻辑和UI验证的业务规则——这些是提取为显式Specification对象的候选
- 定义带有单一IsSatisfiedBy(entity)方法(或用于数据库可推送规格的表达式属性)的Specification接口,封装一个内聚的业务规则
- 为每个原子规则实现具体规格:PremiumCustomerSpec、ActiveAccountSpec、EligibleForDiscountSpec——每个类编码一个命名良好的业务概念
- 在Specification基类上实现组合运算符:And()、Or()、Not()——使复杂规则可以表达为new PremiumCustomerSpec().And(new ActiveAccountSpec()),而非内联布尔逻辑
- 一致地应用规格:在领域验证、查询过滤器(通过表达式树将规格转换为数据库谓词)和UI中使用它们来驱动条件可见性,确保规则定义一次随处使用
['在多个位置重复出现(领域验证、查询过滤器、UI条件)并需要单一规范定义的业务规则,可以复用而无需复制粘贴', '当前在服务方法或仓储查询中表达为难以阅读的布尔链的复杂资格或过滤逻辑', '业务规则应被命名、记录并作为一等领域概念可发现的领域丰富应用,而非匿名lambda表达式', '业务规则频繁添加或更改,需要在不启动完整应用或数据库的情况下独立可测试的系统']
- 为每个规格取一个业务利益相关方能够识别的有意义的领域名称——PremiumCustomerSpecification比CustomerTypeEqualsThreeSpecification更好
- 当规格需要作为数据库查询过滤器应用时,将其实现为表达式树(不仅仅是内存中谓词)——必须加载所有记录才能过滤的纯内存规格是性能反模式
- 使用And/Or/Not运算符组合规格,而非创建重复现有规格逻辑的大型规格——组合是该模式的主要价值
- 通过单元测试独立测试规格,覆盖正向情况(满足规格的实体)和负向情况(不满足的实体),包括边界条件的边缘情况
- 不要在规格中放入超出其所代表单一规则的业务逻辑——检查5个不相关条件的规格是规则引擎,而非规格,应该被分解
- 不要将规格用于基础设施关注点(数据库是否可用?缓存是否预热?)——规格用于领域实体的领域业务规则,而非基础设施状态检查
- 不要让规格依赖外部服务、仓储或I/O——调用数据库或API来评估其谓词的规格无法高效组合,使测试需要模拟基础设施
- 不要对代码库中的每个布尔检查过度应用该模式——在恰好一个地方使用的简单一次性谓词不会从被提取到命名规格类中受益
Sources: SDFrame (Software Design) MetaFrame: 💻 Code & Engineering / 编码与工程