![c1b5d25aac9b3f4bd028bd6f514dc113.png](https://i-blog.csdnimg.cn/blog_migrate/f17979812a4b608961d69f24066abf96.jpeg)
《领域驱动设计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 是输出——是切换之后的结果。
(本篇未完待续)
参考
- ^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.