FP 视角下的领域驱动设计

这周在学习 union type 时偶然学到一个颇有冲击的软件工程思想 -- 领域驱动设计。前端

在了解了这个思想后,我意识到最近很困扰个人 JS 防护式编程的问题有更深的缺陷,那就是领域模型一开始就没定义好。说到领域模型,通常都会联想到后端,特别是 Java 开发。前端的业务逻辑通常不须要上这么复杂的概念。不过,领域驱动设计仍是给了我启发,让我意识到问题出在哪里。编程

我认识领域驱动设计(下简称 DDD)仍是从函数式编程视角入门的。提到 DDD,通常会认为它只和面向对象程序设计有关系,而我所经过 F# 了解到的,ML 系语言的 Hindley–Milner 类型系统,除了能够用来检查类型,还有很重要的做用是它能用来灵活完整地去设计领域模型。后端

假设咱们要定义一个联系人类型:函数式编程

5c54596a08db8

上面的代码用 TypeScript 来表达的话基本长差很少。这个类型定义的问题是它没有传达领域知识:函数

  1. 你不知道哪些字段是可选的单元测试

  2. 你不知道字段的限制。好比,FirstName 只能限制在50个字符之内。学习

  3. 你不知道字段之间的相互关联。好比前三个字段都应该在一个组里面。测试

  4. 你不知道字段的领域逻辑。好比邮箱地址变了后,邮箱认证就要变为 false。ui

上面这些问题,本应该在定义类型的时候就体现出来。而用传统面向对象的类型系统,好比 TypeScript,是作不到的。若是尝试去作的话,会让领域模型代码和实现细节代码混在一块儿。spa

下面来看 F# 的类型系统怎样解决这些问题。

DDD 里面有个术语叫有限上下文(Bounded Context),即在领域模型里面的词语,只有放在当前领域上下文才有意义。这些词语构成了领域模型里面的通用语言(ubiquitous language)。看例子:

5c54597d0e032

这个模块描述了一个纸牌游戏的领域模型。Hand, Player, Deck 等等词汇,只有放在 CardGame 这个有限上下文中才能被理解;而这些词汇就构成了通用语言。上面这段代码不只定义了数据类型,并且定义了领域模型!这种类型定义很是好懂。经过有限上下文和通用语言的建立,咱们能作到“持久性无知”(Persistent Ignorance),即不用懂代码实现也能看懂领域模型。更神奇之处在于,上面的代码不只仅是一个模型描述,并且是一段可执行代码!这体现了代码即设计,设计即代码的思想。

再来反思一下咱们在定义类型时经常忽视的一些问题,好比邮箱地址的数据类型真的只是字符串吗?订单数量的数据类型真的只是整数吗?合法的邮箱地址应该须要通过正则匹配,订单数量经常也会有上下限。用 F# 能够表达以下:

type EmailAddress = EmailAddress of string let createEmailAddress (s:string) =
 if Regex.IsMatch(s,@"^\S+@\S+\.\S+$")
 then Some(EmailAddress s) else None createEmailAddress: string -> EmailAddress option type OrderLineQty = OrderLineQty of int

let createOrderLineQty qty =
 if qty > 0 && <= 99
 then Some(OrderLineQty qty) else None createOrderLineQty: int -> OrderLineQty option 复制代码

Some 和 None 很显式地传达了数据的可能状态,符合模型规约就返回 Some,不然就返回 None。Some 和 None 是 F# 内置的代数数据类型(能够理解为可组合数据类型),它们能够和其它代数数据类型无感知组合。对比下咱们平常用 JS 开发时的作法,不符合要求就返回 undefined 或者 null,而后再在调用处作防护处理。这里的问题是 undefined 和 null 并不能用来传达领域信息,它们没有带上下文就扔给接收者了。(提到这里应该能明白用 Maybe 数据类型和用 _.get 的本质区别了)

再回到一开始抛出的问题,解决办法以下:

type EmailAddress = EmailAddress of string let createEmailAddress (s:string) =
 if Regex.IsMatch(s,@"^\S+@\S+\.\S+$")
 then Some(EmailAddress s) else None createEmailAddress: string -> EmailAddress option type String50 = String50 of string let createString50 (s:string) =
 if s.Length <= 50
 then Some(String50 s) else None createString50: string -> String50 option type PersonalName = {
 FirstName: String50
 MiddleInitial: String50 | option
 LastName: String50
}

type VerifiedEmail = VerifiedEmail of EmailAddress

type VerificationService =
 (EmailAddress * VerificationHash) -> VerifiedEmail option

type EmailContactInfo =
 | Unverified of EmailAddress
 | Verified of VerifiedEmail

type Contact = {
 Name: PersonalName
 Email: EmailContactInfo
}
复制代码

上面的代码不只是完整的领域模型,并且可编译执行。通过领域模型的严格规约,不合法的状态,没法被通用语言表达(这个思想太强大了)。咱们不用再写防护代码了。上面的类型代码就是编译时单元测试。

还值得注意的一点是,随着领域模型的完善,通用语言是在扩展的,好比 VerifiedEmail 等词汇。通用语言的丰富意味着咱们与领域专家(通常是产品需求方,好比产品经理)的理解更容易达成一致。

了解到这些思想后我心里感觉是复杂的。尽管我前一段时间还为别人吐槽 JS 垃圾而不满,但最近我对 JS 的不满也增长了好多。JS 仍然是入门编程性价比比较高的语言,但我不会认为它是最好的语言了……

一方面是它容许一些糟糕写法,没有强制规约,另外一方面是它缺失一些能力,好比静态类型。TypeScript 带来了一堆模板代码,让代码臃肿啰嗦,性价比过低。最重要的是它没法提供本文展现的领域设计能力。如今我开始明白当 Eric Elliott 说 JS 须要的是靠近 Haskell 的类型系统,而不是 Java 的,他想表达的是什么意思。(也有可能我是错的,对 TS 一开始就比较抵触,写的很少)

上面的思考只是对 Domain Modelling Made Functional 一书的仓促总结。更深的含义可能没表达完整。感兴趣的话推荐阅读这本书。

参考: Domain Modeling Made Functional - Scott Wlaschin

相关文章
相关标签/搜索