订单编号的数据类型是什么_领域模型与代数数据类型(第三期)

c1b5d25aac9b3f4bd028bd6f514dc113.png

《领域驱动设计15年》第8章

作者:Scott Wlaschin[1]

译者:封小武

校审:覃宇、伍斌

4. 用代数数据类型来建模

万事俱备,现在我们可以开始真正的建模了。让我们回顾一下文章开头那些对领域的描述,使用代数数据类型来为它们建模。

4.1. 为订单行建模

描述是这样的:“订单行包含订单编号、产品编号以及订购数量”。

描述中有三种不同的特定于领域的基本类型,我们先定义三种类型:

type OrderId = OrderId of int
type ProductId = ProductId of int
type OrderQty = OrderQty of int 

然后定义一个记录,用 AND 把它们组合起来。

type OrderLine = {
  OrderId : OrderId
  ProductId : ProductId
  OrderQty : OrderQty
}

4.2. 为联系信息建模

描述是这样的:“联系信息由人名和联系方式组成,联系方式可以是邮箱地址,也可以是电话号码”。

这里又出现了三种“基本”领域类型:

type PersonalName = PersonalName of string
type EmailAddress = EmailAddress of string
type PhoneNumber = PhoneNumber of string

接下来,我们可以定义一个选项是 EmailAddress 或 PhoneNumber 的选择类型。

type ContactMethod =
 | ByEmail of EmailAddress
 | ByPhone of PhoneNumber

最后,我们把 PersonalName 和 ContactMethod 组合成一个记录:

type ContactInformation = {
  Name : PersonalName
  ContactMethod : ContactMethod
}

4.3. 为买家建模

描述是这样的:“买家可能是一次性‘访客’,也可能已经在网站上注册了。注册过的买家会被分配客户编号,而一次性访客没有”

我们先定义两种不同的类型,分别代表两类访客,外加一个基本类型的 CustomerId:

type GuestPurchaser = {
  Name : PersonalName
  ContactMethod : ContactMethod
}
 
type CustomerId = CustomerId of int
 
type RegisteredPurchaser = {
  Id: CustomerId
  Name : PersonalName
  ContactMethod : ContactMethod
}

然后,我们创建一个选择类型,它包括上面这两种选项。

type Purchaser =
 | Guest of GuestPurchaser
 | Registered of RegisteredPurchaser

4.4. 为邮箱地址建模

描述是这样的:“邮箱可能经过了验证也可能没有经过验证。经过验证的邮箱才可以接收密码重置邮件”。

区分 Unvalidated 邮箱和 Validated 邮箱非常重要——它们有着不同的业务规则,因此我们应该要定义两种不同的类型来代表两种邮箱,再用它们组成选择类型:

type UnvalidatedEmailAddress = UnvalidatedEmailAddress of string
type ValidatedEmailAddress = ValidatedEmailAddress of string
 
type EmailAddress =
  | Unvalidated of UnvalidatedEmailAddress
  | Validated of ValidatedEmailAddress

我们可以把重置密码的流程写成这样:

type SendPasswordResetLink =
  ResetLinkUrl -> ValidatedEmailAddress -> output???

我们还不确定输出是什么,所以暂时不去定义。另外,邮件发送的细节(比如通过 SMTP 服务器发送)现在与领域模型还没什么关系,因此也不会在这里记录。

最后,我们可以将邮箱的验证过程记录如下:

type ValidationToken = ValidationToken of string // 一个没有字面意义的 token
 
type ValidateEmailAddress =
  UnvalidatedEmailAddress -> ValidationToken -> ValidatedEmailAddress

上面这段代码读作:提供一个未经验证的邮箱地址和一个验证 token,我们就能得到一个已验证的邮箱地址。

4.5. 记录失败

但这里还存在问题,我们之前说过,验证操作可能失败。这就要在两种输出之间做出选择。如果一切顺利,得到的是 ValidatedEmailAddress;但如果出现错误,得到的则是某种错误消息。我们可以创建另一种代表验证结果的选择类型来解决这个问题:

type ValidationResult =
 | Ok of ValidatedEmailAddress
 | Error of string

然后我们就可以使用 ValidationResult 作为验证流程的输出了,用它来代替

 ValidatedEmailAddress。
type ValidateEmailAddress =
  UnvalidatedEmailAddress -> ValidationToken -> ValidationResult

如果我们想让可能发生的错误类型更加明确,可以使用另一种选择类型 ValidationError 来记录错误,然后在结果定义中使用这个类型:

type ValidationError =
 | WrongEmailAddress
 | TokenExpired
 
type ValidationResult =
 | Ok of ValidatedEmailAddress
 | Error of ValidationError

这种表示“结果”的类型很常见,大多数函数式语言中都内建了类似的泛型类型,使用泛型的定义可能是:

type Result<'SuccessType,'FailureType> =
 | Ok of 'SuccessType
 | Error of 'FailureType

如果使用内建类型,验证流程的函数定义如下:

type ValidateEmailAddress =
  UnvalidatedEmailAddress -> ValidationToken -> Result<ValidatedEmailAddress,ValidationError>

上面的定义清楚地表达了验证可能会失败,以及可能出现的错误。而且,这是代码,不是文档,我们可以确保任何实现都能和设计准确地匹配。

4.6. 组合多个类型来勾勒领域模型

这种建模并不是“提前做大量设计”的重量级方法。恰恰相反,在与领域专家讨论时这种方法有助于齐心协力地绘制“设计草图”。

举个例子,假设我们要跟踪一家电子商务网站的支付业务。我们来看看如何在设计研讨时用代码描绘设计。

我们先从 CheckNumber 这样的基本类型包装器开始说起。它们就是前面讨论过的“基本类型”。使用包装器可以赋予它们有意义的命名,这样可以让它们更容易被其它领域模型理解。

type CheckNumber = CheckNumber of int
type CardNumber = CardNumber of string

我们接下来深入地讨论一下信用卡(Credit Card),这可能需要创建更多低层级的类型。CardType 是 OR 类型——它要在 Visa 或(OR)Mastercard 之间做出选择;CreditCardInfo 则是 AND 类型,它是一个包含 CardType 和(AND)CardNumber 的记录:

type CardType = Visa | Mastercard
 
type CreditCardInfo = {
  CardType : CardType
  CardNumber : CardNumber
}

我们了解到支付业务可以接受现金(Cash)、支票(Check)或信用卡(Card)三种支付方式,所以我们还需要再定义一个 OR 类型 PaymentMethod,它要在 Cash 或(OR)Check 或(OR)Card 之间作出选择。这不再是一个简单的“枚举”,因为有些选项有关联的数据:Check 要关联 CheckNumber,而 Card 则要关联 CreditCardInfo。

type PaymentMethod =
 | Cash
 | Check of CheckNumber
 | Card of CreditCardInfo

接下来我们要讨论的是金额,它需要定义的类型更多了,比如 PaymentAmount 和Currency:

type PaymentAmount = PaymentAmount of decimal // 必须是正数
type Currency = EUR | USD

最后才是位于最顶层的 Payment 类型,它是一个包含 PaymentAmount 和(AND)Currency 和(AND)PaymentMethod 的记录:

type Payment = {
  Amount : PaymentAmount
  Currency: Currency
  Method: PaymentMethod
}

建模到此为止。我们只用了大约 25 行代码就完成了领域建模,而这些代码定义出了一组非常有意义的类型,它们具备实现的指导意义。

当然,这些类型没有直接相关的行为,因为它们是函数式的模型,不是面向对象的模型。要记录它们可以采取的操作,我们可以改用函数类型的定义。

例如,如果我们想要说明有这么一种方法,它可以使用 Payment 类型来支付未付款账单,最终得到的结果是支付过的账单,我们可能会定义出下面这个函数类型:

type PayInvoice = UnpaidInvoice -> Payment -> PaidInvoice

这段代码可以读作:提供一个 UnpaidInvoice 和一个 Payment,就可以创建一个 PaidInvoice。

或者想要切换支付币种:

type ConvertPaymentCurrency = Payment -> Currency -> Payment

第一个 Payment 是输入,第二个参数(Currency)是转换后的币种,第二个 Payment 是输出——是切换之后的结果。

(本篇未完待续)

参考

  1. ^Scott Wlaschin is a developer, architect and author. He is the author of the popular F# site fsharpforfunandprofit.com, and the book "Domain Modeling Made Functional", published by Pragmatic Bookshelf.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值