如何在多个抽象维度上对复杂类型建模?

问题描述 投票:0回答:1

背景

我正在尝试使用 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模型的示例:

要求 1) 付款状态

付款必须处于有效状态(NewInProgressComplete)。 Payment 可以更改为 InProgress 当且仅当 PaymentNew Payment 可以更改为 Complete 当且仅当 PaymentInProgress

要求2)付款结果

付款Complete必须有一个有效的结果(ApprovedCancelledDeclinedFailedApproved Payment 需要 ApprovedAmount 取消拒绝失败付款需要原因

要求3)付款信息

所有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
}
types functional-programming f# domain-driven-design discriminated-union
1个回答
0
投票

我觉得你很亲近。您需要做的就是对每个维度进行完整建模,然后将它们组合成一个记录类型,如下所示:

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 个状态,因此它肯定应该是产品类型(即记录),而不是总和类型(即联合)。这些是代数数据类型

© www.soinside.com 2019 - 2024. All rights reserved.