内容简介:这周在学习 union type 时偶然学到一个很有冲击的软件工程思想 -- 领域驱动设计。在了解了这个思想后,我意识到最近很困扰我的 JS 防御式编程的问题有更深的缺陷,那就是领域模型一开始就没定义好。说到领域模型,一般都会联想到后端,特别是 Java 开发。前端的业务逻辑一般不需要上这么复杂的概念。不过,领域驱动设计还是给了我启发,让我意识到问题出在哪里。我认识领域驱动设计(下简称 DDD)还是从函数式编程视角入门的。提到 DDD,一般会认为它只和面向对象程序设计有关系,而我所通过 F# 了解到的,ML
这周在学习 union type 时偶然学到一个很有冲击的软件工程思想 -- 领域驱动设计。
在了解了这个思想后,我意识到最近很困扰我的 JS 防御式编程的问题有更深的缺陷,那就是领域模型一开始就没定义好。说到领域模型,一般都会联想到后端,特别是 Java 开发。前端的业务逻辑一般不需要上这么复杂的概念。不过,领域驱动设计还是给了我启发,让我意识到问题出在哪里。
我认识领域驱动设计(下简称 DDD)还是从函数式编程视角入门的。提到 DDD,一般会认为它只和面向对象程序设计有关系,而我所通过 F# 了解到的,ML 系语言的 Hindley–Milner 类型系统,除了可以用来检查类型,还有很重要的作用是它能用来灵活完整地去设计领域模型。
假设我们要定义一个联系人类型:
上面的代码用 TypeScript 来表达的话基本长差不多。这个类型定义的问题是它没有传达领域知识:
-
你不知道哪些字段是可选的
-
你不知道字段的限制。比如,FirstName 只能限制在50个字符以内。
-
你不知道字段之间的相互关联。比如前三个字段都应该在一个组里面。
-
你不知道字段的领域逻辑。比如邮箱地址变了后,邮箱认证就要变为 false。
上面这些问题,本应该在定义类型的时候就体现出来。而用传统面向对象的类型系统,比如 TypeScript,是做不到的。如果尝试去做的话,会让领域模型代码和实现细节代码混在一起。
下面来看 F# 的类型系统怎样解决这些问题。
DDD 里面有个术语叫有限上下文(Bounded Context),即在领域模型里面的词语,只有放在当前领域上下文才有意义。这些词语构成了领域模型里面的通用语言(ubiquitous language)。看例子:
这个模块描述了一个纸牌游戏的领域模型。Hand, Player, Deck 等等词汇,只有放在 CardGame 这个有限上下文中才能被理解;而这些词汇就构成了通用语言。上面这段代码不仅定义了数据类型,而且定义了领域模型!这种类型定义非常好懂。通过有限上下文和通用语言的创建,我们能做到“持久性无知”(Persistant 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 一书的仓促总结。更深的含义可能没表达完整。感兴趣的话推荐阅读这本书。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 人工智能在系统领域面临的挑战:伯克利视角
- 架构视角 - DDD、TDD、MDD领域驱动、测试驱动还是模型驱动?
- 企业视角看攻防演练
- 另一个视角看待这次 antd
- 架构视角:文件的通用存储原理
- 许式伟:架构设计的宏观视角
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。