當業務規則變得「可組合、可拆分、可重組」時,Specification Pattern(規約模式) 是領域驅動設計(DDD)中最經典的解法。本文以「書店租書驗證」為例:需同時滿足「未達最大租書數」與「帳號啟用中」兩條規則,展示如何將規則碎片化為獨立類別,再透過 And/Or/Not 邏輯運算子自由組合。
public interface ISpecification<T> {
bool IsSatisfiedBy(T entity);
ISpecification<T> And(ISpecification<T> other);
ISpecification<T> Or(ISpecification<T> other);
ISpecification<T> Not();
}
public abstract class CompositeSpecification<T> : ISpecification<T> {
public abstract bool IsSatisfiedBy(T entity);
public ISpecification<T> And(ISpecification<T> spec)
=> new AndSpecification<T>(this, spec);
public ISpecification<T> Or(ISpecification<T> spec)
=> new OrSpecification<T>(this, spec);
public ISpecification<T> Not()
=> new NotSpecification<T>(this);
}
public class AndSpecification<T> : CompositeSpecification<T> {
private readonly ISpecification<T> _left, _right;
public AndSpecification(ISpecification<T> left, ISpecification<T> right) {
_left = left; _right = right;
}
public override bool IsSatisfiedBy(T entity)
=> _left.IsSatisfiedBy(entity) && _right.IsSatisfiedBy(entity);
}
public class OrSpecification<T> : CompositeSpecification<T> { /* || */ }
public class NotSpecification<T> : CompositeSpecification<T> { /* ! */ }
public class HasReachedMaxSpec : CompositeSpecification<Customer> {
public override bool IsSatisfiedBy(Customer c) => c.TotalRentNumber >= 6;
}
public class CustomerActiveSpec : CompositeSpecification<Customer> {
public override bool IsSatisfiedBy(Customer c) => c.Active;
}
public class Customer {
private readonly ISpecification<Customer> _maxSpec, _activeSpec;
public Customer(ISpecification<Customer> max, ISpecification<Customer> active) {
_maxSpec = max; _activeSpec = active;
}
public int TotalRentNumber { get; set; }
public bool Active => true; // 簡化
public bool CanRent() {
// 未達上限 AND 帳號啟用
var spec = _maxSpec.Not().And(_activeSpec);
return spec.IsSatisfiedBy(this);
}
}
| 優點 | 缺點/注意 |
|---|---|
| 規則單一職責、易測試、可動態組合 | 簡單 if-else 反而更清楚時別濫用 |
| 符合開放封閉原則,新增規則不改舊碼 | 過度拆解導致類別爆炸,需把握粒度 |
| 可序列化存庫,支援規則引擎/動態配置 | 效能敏感路徑(如迴圈內萬次判斷)慎用 |
規約模式的精髓在於 「把業務規則變成一等公民」。下次看到 if (a && b && !c || d) 橫跨幾十行,請考慮拆成 SpecA.And(SpecB).And(SpecC.Not()).Or(SpecD)——程式碼會開始說人話。