如何在多抽象维度建模复杂类型?基于函数式DDD的Payment案例
背景
我尝试采用Scott Wlaschin所著《领域建模的函数式方法》(Domain Modeling Made Functional)中的函数式DDD风格建模,核心思路是将单一类型(比如Payment)拆分为多个类型,精准匹配原类型不同“状态”下的数据结构。
Scott在书中第84页的示例里,把Order拆分为UnvalidatedOrder和ValidatedOrder,用联合类型表示Order:
type Order = | UnvalidatedOrder of { ... } | ValidatedOrder of { ValidatedAt: DateTime; ... }
此处使用类F#伪代码简化表述
但这种方案只针对“验证”这单一变化维度有效,如果类型存在两个变化维度,该怎么处理?
Payment建模示例
以Payment模型为例,需求如下:
需求1:Payment状态流转
Payment需处于New、InProgress、Complete三种有效状态;仅New状态可转为InProgress,仅InProgress状态可转为Complete。
需求2:Payment完成结果
处于Complete状态的Payment需要包含结果:Approved、Cancelled、Declined、Failed。其中Approved需附带ApprovedAmount,其余结果需附带Reason。
需求3:基础信息
所有Payment都必须包含唯一Guid标识和RequestedAmount。
我的初始建模尝试
type PaymentResult = | Approved of { ApprovedAmount: decimal } | Cancelled of { Reason: string; } | Declined of { Reason: string; } | Failed of { Reason: string; } type PaymentStatus = | New | InProgress of { StartedAt: DateTime; } | Complete of { StartedAt: DateTime; CompletedAt: DateTime; } type Payment = { Id: Guid; RequestedAmount: decimal; Status: PaymentStatus; } type StartPayment = (Status: PaymentStatus.New) (Now: DateTime) -> PaymentStatus.InProgress type FinishPayment = (Status: PaymentStatus.InProgress) (Now: DateTime) (Result: PaymentResult) -> PaymentStatus.Complete
这个方案的问题在于,部分Payment相关信息被塞进了PaymentStatus里,但PaymentStatus应该只存储和状态本身相关的数据。
按照Scott的方式拆分后的模型
按照Scott的思路拆分Payment后,得到如下模型:
type Payment = | NewPayment of { Id: Guid; RequestedAmount: decimal; } | InProgressPayment of { Id: Guid; RequestedAmount: decimal; StartedAt: DateTime; } | CompletedPayment of { Id: Guid; RequestedAmount: decimal; StartedAt: DateTime; CompletedAt: DateTime; Result: PaymentResult; }
这个模型把Payment的所有数据都保留在各自的类型记录中,但如果新增“任意状态的Payment都可被废弃”的需求,仅针对废弃维度建模会得到:
type Payment = | NonAbandonedPayment of { ... } | AbandonedPayment of { AbandonedAt: DateTime; ... }
如果要同时结合“状态”和“废弃”两个维度,就需要创建6个类型来覆盖所有组合:
type Payment = | NonAbandonedNewPayment of { Id: Guid; RequestedAmount: decimal; } | AbandonedNewPayment of { Id: Guid; RequestedAmount: decimal; AbandonedAt: DateTime; } | NonAbandonedInProgressPayment of { Id: Guid; RequestedAmount: decimal; StartedAt: DateTime; } | AbandonedInProgressPayment of { Id: Guid; RequestedAmount: decimal; StartedAt: DateTime; AbandonedAt: DateTime; } | NonAbandonedCompletePayment of { Id: Guid; RequestedAmount: decimal; StartedAt: DateTime; CompletedAt: DateTime; Result: PaymentResult; } | AbandonedCompletePayment of { Id: Guid; RequestedAmount: decimal; StartedAt: DateTime; CompletedAt: DateTime; Result: PaymentResult; AbandonedAt: DateTime; }
这会产生维度的笛卡尔积,哪怕场景简单也显得非常繁琐。
技术问题
在遵循“类型相关信息均存储于该类型内”的前提下,该如何表示类型的多抽象变化维度?我目前想到的最优方案是放弃这个前提,把PaymentStatus和AbandonedStatus作为属性嵌入单一Payment类型:
type AbandonedStatus = | NotAbandoned | Abandoned type PaymentStatus = | New | InProgress | Complete type Payment = { Id: Guid; RequestedAmount: decimal; Status: PaymentStatus; AbandonedStatus: AbandonedStatus }
内容的提问来源于stack exchange,提问作者Tyler Kasper

