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

efb9e76f463cda2ea049701c83fd943e.png

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

作者:Scott Wlaschin[1]

译者:封小武

校审:覃宇、伍斌

作者评语:很荣幸能为本书撰写文章。虽然《领域驱动设计》一书出版已经有十五个年头了,但书中的洞见和智慧历久弥新,持续影响着一代又一代的程序员新人。但自 Evans 落笔那一刻开始,变化已经悄然发生——函数式编程逐渐兴起,成为了和面向对象编程并驾齐驱的编程范式。函数式编程的文章大多侧重数学理念,但我认为,将函数式编程和领域驱动设计的原则结合起来,很有可能产生有效的设计。我将我撰写的《Domain Modeling Made Functional》一书浓缩成一章来解释这背后的原因。

促进开发团队、领域专家与其他干系人之间的顺畅交流是领域驱动设计的主要目标之一。实际上,实现代码差不多应该和领域专家的心智模型一一对应。也就是说,如果领域专家将某个事物称为“Order”,那代码就中就应该也有一个“Order”术语,而且它的行为要和专家口中的“Order”一致。反过来说,我们的代码中不应当包含领域专家的模型中没有体现的事物。这意味着像“OrderFactory”、“OrderManager”、“OrderHelper"这样的术语不应该出现。当然,代码库中难免会出现一些技术术语,但是我们应该避免暴露这部分设计。

于是挑战来了:我们的代码到底和领域有多匹配?我们能够创造出读起来像文本一样的代码,让非开发人员也能理解吗?我们能够避免引入像“Manager”和“Factory”这样的非领域术语吗?我认为在领域建模过程中使用代数数据类型可以解决所有这些问题。

1. 发现领域建模中的常见概念

当我们还在探索阶段和领域专家交谈的时候,常常会听到关于领域的口头描述。例如:

  • “订单行包含订单编号、产品编号以及订购数量”
  • “联系信息由人名和联系方式组成,联系方式可以是邮箱地址,也可以是电话号码”
  • “邮箱可能经过了验证也可能没有经过验证。经过验证的邮箱才可以接收密码重置邮件”
  • “买家可能是一次性‘访客’,也可能已经在网站上注册了。注册过的买家会被分配一个客户编号,而一次性访客没有客户编号”
  • “结账时,先看到的是未支付的账单和支付信息,最后得到的是已经支付过的账单”

我们将基于这些和有关领域的描述来构建统一语言,一些常见的模式会在这个过程中浮现出来,我们可以对这些模式进行分类:

  • 基本类型的值
  • 被视为整体的一组事物
  • 事物之间的选择
  • 工作流(还有用例、场景等其它叫法)
  • 状态和生命周期

接下来我们将深入每一种模式,探讨它们的细节。

1.1. 基本类型的值

领域专家从不会说“整型”或者“字符串”。他们会使用像“订购数量(Order Quantity)”或“邮箱地址(Email Address)”这样的领域概念。这些概念可以用一个整型或者一个字符串表示,但是它们并不是等价的。例如,“订单编号(Order Id)”、“产品编号(Product Id)”以及“订购数量”也许都可以用整数表示,但是领域概念绝不等同于整数。例如,“订单编号”乘以 2 没有任何道理。

即便概念就是整数也不能直接和编程语言中的 int 对应——几乎不可能没有约束。例如,“订购数量”必须至少为 1,而且还可能存在上限,比如 100。销售系统不可能允许订购 20 亿件产品!

同样,“邮箱地址”和“电话号码(Phone Number)”都可以用字符串表示,但也不可互换,而且这两个概念都要满足特定的约束。

如果我们用文档来记录这些基本类型的值,可能会这样写:

data OrderId              // 字面无意义 -- 表现形式不重要
data ProductId            // 字面无意义 -- 表现形式不重要
data OrderQuantity is int // 大于等于 1 小于等于 100
data PersonalName is string // 不能为 null,也不能是空字符串,不能超过 100 个字符。
data EmailAddress is string // 不能为 null,也不能是空字符串,必须包含字符 @。
data PhoneNumber is string  // 不能为 null,也不能是空字符串,只能由数字、圆括号或者连接号

将上述记录转换成代码的时候,这些描述的简单性应当被保留下来。代码的表现形式应该尽可能贴近领域中的概念。

1.2. 被视为整体的一组事物

显然,有些领域概念的值不是基本类型,它们是由这些较小的值组合而成。对于这些类型的事物,我们在记录时会使用“AND”(和)将值组合在一起,比如:

data OrderLine is OrderId AND ProductId AND OrderQuantity
data ContactInformation is PersonalName AND ContactMethod

2. 事物之间的选择

由不同的选项组成的概念是另一种常见的模式。

  • “邮箱要么验证过(Validated),要么没验证过(Unvalidated)”
  • “买家(Purchaser)要么是一次性‘访客’(Guest),要么是注册(Registered)用户”
  • “账单(Invoice)要么未支付(Unpaid),要么已支付(Paid)”

我们可以使用“OR”(或)来记录这些情形,就像下面这样:

data Email is UnvalidatedEmail OR ValidatedEmail
data ContactMethod is EmailAddress OR PhoneNumber
data Purchaser is GuestPurchaser OR RegisteredPurchaser (contains CustomerId)
data Invoice is UnpaidInvoice OR PaidInvoice

这些选项概念往往是业务规则的关键,一定要清楚地认识到这一点。例如:

  • 只有 ValidatedEmail 才可以接收重置密码链接(而 UnvalidatedEmail 不能接收)。
  • 只有 RegisteredPurchaser 才可以享受折扣(而 GuestPurchaser 无法享受)。
  • 只能为 UnpaidInvoice 付款(而 PaidInvoice 不能付款)。

如果分不清不同选项之间的差别,只会得到含混模糊的设计,而最终的实现可能漏洞百出。

2.1. 克制采用类驱动设计的冲动

持久化无关是领域驱动设计的关键原则。这一原则非常重要,它强调将重点放在如何准确地对领域进行建模,而不要去关注数据库中具体的数据表现形式。实际上,经验丰富的面向对象开发者对这种不要被特定数据库模型左右设计的思想并不陌生,面向对象中的依赖注入等技术支持将数据库实现与业务逻辑分开。

但是,当我们在思考领域的设计时,要小心别让对象和类喧宾夺主,导致设计跑偏。例如,当领域专家谈起不同的联系方式时,你可能不由自主地在脑海里构思下面这样的类层级结构:

// 代表全部联系方式
class ContactMethodBase ...    
// 代表邮箱联系方式
class EmailAddressContactMethod extends ContactMethodBase ...  
// 代表电话号码联系方式
class PhoneNumberContactMethod extends ContactMethodBase ...   

然而,让类来驱动设计和让数据库来驱动设计一样危险,这种方法也不能真正地抓住需求。

  • 我们脑海里的类层级结构中引入了一个人为创造的基类 ContactMethodBase,它在现实世界中并不存在。这是对领域概念的曲解。问问领域专家知不知道 ContactMethodBase 是什么!
  • 同样,EmailAddress 是可以重用的基本类型的值,它并不是一种特定的联系方式,因此我们不得不创建一个包装类 EmailAddressContactMethod,才能在当前特定的上下文里使用它。这又是一个在代码中人为创造的对象,它并不代表领域中的某个事物。

这里的经验是,在收集需求的时候保持开放的心态,不要在领域概念上强加技术思想。

2.2. 工作流

到目前为止,我们一直在讨论领域中的“名词”——数据结构。实际上,对模型来说最重要的并非是名词。为什么这么说呢?

业务不仅包含数据,还需要转换数据。也就是说,典型的业务流程可以被想象成一系列对数据和文档的转换。业务的价值在转换的过程中产生,因此理解这些转换如何工作、如何关联至关重要。

静态数据(放在那里从来不会用的数据)不提供任何价值。那么是什么让人(或者自动化流程)开始使用数据并创造价值呢?触发点通常来自外部(收到一封邮件或者接到一通电话),有的也来自时间(每天早上十点要做的某项工作),还有的来自观测(收件箱里已经没有待处理的订单了,该做其他工作了)。

无论是什么触发了流程,重要的是在设计中将它们体现出来。我们通常将这些触发点称为领域事件。我们要建模的业务流程几乎全部都是从领域事件开始。例如:

  • “支付账单已接收”(Payment Received)是领域事件,它将启动“支付”(Applying Payment)工作流
  • “邮箱验证链接已点击”(Email Validation Link Clicked)是领域事件,它将启动“验证邮箱”(Validate Email)工作流

在领域中探索并发现事件的方法有很多,其中事件风暴最适合领域驱动设计。Alberto Brandolini 创造的这种协作过程,可以发现业务事件以及与之相关的工作流。

假如我们已经识别出了有意义的事件,接下来就要把这些事件触发(Trigger)的工作流(或者用例)记录在案。我们尽量保持宏观,只用记下每个工作流(Workflow)的输入(Input)和输出(Output),就像这样:

workflow ApplyPaymentToInvoice =  
   triggered by: PaymentReceived
   inputs: UnpaidInvoice, Payment
   outputs: PaidInvoice    
 
workflow ValidateEmail =
   triggered by: EmailValidationLinkClicked
   inputs: UnvalidatedEmail, ValidationToken (from clicked link)
   outputs: ValidatedEmail OR an error

第二个例子中的邮箱验证工作流可能会失败(比如链接过期),因此存在不同的输出选择:要么是验证过的邮箱地址,要么是错误。

2.3. 状态和生命周期

重要的业务实体大多存在生命周期——它们会随着时间推移经历一系列变化。

  • 账单刚开始是未支付的,然后转换为已支付的。
  • 买家刚开始是访客,然后转换为已注册的。

即便是简单的值也可能经历状态的转换。例如,电子邮件地址一开始是未验证的,然后转换为已验证的。

捕获状态以及状态之间的转换是领域建模的一项重要工作。我们可以复用前面介绍的技术来捕获状态及其转换:用一组选项来表示不同的状态,用工作流表示状态之间的转换。例如,账单的状态及其转换可以这样记录:

// 两种状态
data Invoice = UnpaidInvoice OR PaidInvoice
// 一种转换
workflow ApplyPaymentToInvoice =  
    transforms UnpaidInvoice -> PaidInvoice

这个例子里没有PaidInvoice 状态到 UnpaidInvoice 状态的转换。这种转换应该存在吗?从状态和生命周期的角度进行思考是一种不错的方法,它能激发出关于设计的富有成效的讨论。

(本章未完待续)

参考

  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、付费专栏及课程。

余额充值