我正在尝试使用 Scott Wlaschin 在“Domain Modeling Made Functional”中提出的功能性 DDD 风格。我特别感兴趣的一点是将单一类型(在我们的例子中是 Payment)分解为更准确地表示原始类型不同“状态”中可用数据的单独类型的概念。在第 84 页的 Scott 示例中,他将 Order 类型分为 UnvalidatedOrder 类型和 ValidatedOrder 类型。因此,您可以将 Order 类型描述为联合类型,如下所示
type Order =
| UnvalidatedOrder of { ... }
| ValidatedOrder of { ValidatedAt: DateTime; ... }
为简洁起见,我在这里使用类似 F# 的伪代码
这很有效,因为这里的 Order 类型只是“一维”的变体,即“验证维度”。那么如果我们有一个在两个“维度”上不同的类型呢?
让我们考虑具有以下要求的Payment模型的示例:
付款必须处于有效状态(New、InProgress 或 Complete)。 Payment 可以更改为 InProgress 当且仅当 Payment 是 New Payment 可以更改为 Complete 当且仅当 Payment 是 InProgress
付款Complete必须有一个有效的结果(Approved,Cancelled,Declined,Failed) Approved Payment 需要 ApprovedAmount 取消、拒绝或失败付款需要原因
所有Payments必须有一个独特的
Guid
来跟踪它们
所有Payments必须有一个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
我对这种尝试的担忧是,我们正在 PaymentStatus 类型中存储有关 Payment 的信息。在我的理解中,PaymentStatus包含的数据应该只是关于PaymentStatus的信息。
我们可以像 Scott 那样通过拆分 Payment 类型来解决这个问题,我们得到
type Payment =
| NewPayment of { ... }
| InProgressPayment of { StartedAt: DateTime; ... }
| CompletedPayment of {
StartedAt: DateTime;
CompletedAt: DateTime;
Result: PaymentResult;
...
}
这个模型似乎在Payment记录中保留了与Payment相关的数据。
现在如果我们添加能够放弃 Payment 的概念,不管它的状态如何。
如果我们只是对废弃的Payments建模,我们会得到
type Payment =
| NonAbandonedPayment of { ... }
| AbandonedPayment of { AbandonedAt: DateTime; ... }
但是,在结合“被遗弃”和“状态”这两个“维度”时,我必须创建 6 个单独的类型来表示所有情况
type Payment =
| NonAbanonedNewPayment
| AbandonedNewPayment
| NonAbandonedInProgressPayment
| AbandonedInProgressPayment
| NonAbandonedCompletePayment
| AbandonedCompletePayment
这导致 Payment 的两个“维度”的笛卡尔积。这已经很笨重了,这是我能想到的最简单的案例。
假设所有与类型相关的信息都应存储在该类型中,您将如何表示一个类型的这些多个维度?
我目前的最佳答案是放弃问题的前提,并简单地定义一个 PaymentStatus 类型和一个 AbandonedStatus 类型用作单个 Payment 类型中的属性,如我在示例中的初始尝试中给出的那样。
type AbandonedStatus =
| NotAbandoned
| Abandoned
type PaymentStatus =
| New
| InProgress
| Complete
type Payment = {
Status: PaymentStatus;
AbandonedStatus: AbandonedStatus
}
我觉得你很亲近。您需要做的就是对每个维度进行完整建模,然后将它们组合成一个记录类型,如下所示:
type PaymentStatus =
| NewStatus of { ... }
| InProgressStatus of { StartedAt: DateTime; ... }
| CompletedStatus of {
StartedAt: DateTime;
CompletedAt: DateTime;
Result: PaymentResult;
...
}
type PaymentAbandonment =
| NonAbandoned of { ... }
| Abandoned of { AbandonedAt: DateTime; ... }
type Payment =
{
Status : PaymentStatus
Abandonment : PaymentAbandonment
}
由于您最终的付款类型有 3x2=6 个状态,因此它肯定应该是产品类型(即记录),而不是总和类型(即联合)。这些是代数数据类型。