10 月 23 - 25 日,QCon 上海站即将召开,现在购票,享9折优惠 了解详情
写点什么

使用函数式语言建立领域模型

  • 2021-06-03
  • 本文字数:4033 字

    阅读完需:约 13 分钟

使用函数式语言建立领域模型

领域模型=代码

如果说敏捷软件开发主张面对面沟通,通过快速迭代的手段,让有价值的软件尽早面向市场,从而适应快速变化的需求。

那么 DDD 则为敏捷开发过程中的沟通形式,作出了进一步的补充。纵观 DDD 的所有环节,无一不是在打通领域专家和开发人员之间的沟通和交流。DDD 的精髓在于通过让开发人员理解领域,进而让开发人员使用编程语言建立一个跟领域专家脑海中一致的领域模型,使得该领域模型成为大家共享知识的途径,这将有效的减少不同利益相关者的沟通及交流,确保所有人都在解决同一个问题。


共享模型=代码=文档

我还记得我的第一份工作,每当有代码或者设计改动时,都要去更新 UML 类图以及数据库设计文档。这些文档大概充当着共享模型的作用,但是最终这些设计类图和文档都慢慢变得不可信,因为没有任何手段保证文档会被及时跟新。实际上代码比较擅长表达设计内容,从本质上讲,源代码是一个文档,可以完美地描述产品的每个当前设计决策。原则上,如果开发人员用代码创建了一个领域专家脑海中一致的领域模型,则代码无疑是最有效,最实时的共享模型。唯一制约这个等式的鸿沟在于领域专家能否读懂代码。而简单易学,有表达力,直观的编程语言则能在创建领域模型的过程中占领一些优势。

领域建模

领域建模是整个 DDD 环节中最最考验开发人员功底的一环,不同于传统的数据库建模技术,开发人员需要有很好的抽象能力,通过恰如其分的编程技术,将领域知识映射到一个代码模型中。长期以来 OO 语言被认为是领域建模的首选,一些 OO 的技巧可以很好的用来抽象领域模型。而函数式语言则被普遍认为只能用来做数据处理,科学计算等。本文将为大家展示如何通过函数式编程语言进行领域建模,本文选用 TypeScript 编写实例,TypeScript 类型系统完全满足函数式编程需求,当然本文也适用于其他拥有静态类型系统并拥有代数数据类型的函数式编程语言。

TypeScript 的类型系统

相对于 OO,你只需要知道少量的语法知识就可以开始领域建模了,从这个角度来讲,实际上代数数据类型更适合领域建模,从而让领域模型成为文档。

类型

各类编程语言在设计的时候就已经提供了类似string, bool, number等简单类型(primitive),然而在真实世界里面,你还需要将这些类型组合成更大的类型,从而来映射现实世界。在 TypeScript 中,type关键字用来组合更大的类型:


type Name = {  firstName: string  middleName: string  lastName: string}
复制代码


上面类型的用途是显而易见的,除此之外type还有起别名的用途,不要小瞧这个特性,他可以帮助你把领域知识记载在你的领域模型中,考虑下面的代码:const timeToFly = 10你能一眼看出这句代码代表的领域知识吗?也许不能,fly 多久?查文档?No,你应该时刻告诉自己,代码等于文档。改进后的代码如下:


type Second = numberconst timeToFly: Second = 10
复制代码

Or 类型

在 TypeScript,这种类型被称为联合(Union Types),通过符号|来创建,考虑下面的类型:type Pet = Fish | BirdPetFish或者是Bird类型。一般来说函数式语言都会有强大的模式匹配能力,来处理这种或类型,然而受制于 TypesScript 没有模式匹配或者说能力很弱,通常情况下,会在类型里面添加一个字符串字面量, 从而来区分不同的类型, 在次不再细说。

And 类型

在 Typescript 中,这种类型被称为交叉类型(Intersection Types),通过符号&来创建,考虑下面的类型:type ABC = A & B & C表示 ABC 类型包含所有 A、B、C 三个类型里面的属性。

定义函数类型

在 TypeScript 中,函数与其他类型没什么区别,也可以通过type关键字来定义,例如:type Add = (a: number) => (b: number) => numberAdd是一个函数,接收两个类型为 number 的类型ab,返回 number。

通过代码来共享领域知识

type CreditCard = {  cardNo: string  firstName: string  middleName: string  lastName: string  contactEmail: Email  contactPhone: Phone}
复制代码

通过前面介绍的知识,我们很容易就可以写出上面的代码,用来描述CreditCard这种支付方式。注意我们没有使用class。但这是一个靠谱的领域模型吗?如果不靠谱,它的问题在哪里?这段代码最大的问题是他没有把本该拥有的领域知识记录在其中,我来试着问你几个问题:问:middle name可以为空吗?答 1:不清楚,也许需要查文档。答 2:也许可以吧?middle name可以为null

为可空类型建模

在函数式编程语言中,可空类型被定义为 Option,虽然 null 在 ts 中是合法的(注:我们可以通过 strictNullChecks 来强致 null 检查),但是在函数式编程语言中,你只能通过 Option 类型来表达可空类型。当领域专家告诉你middle name可以存在,或者为空。注意用词“或”,说明我们可以通过 Union 类型来为可空类型建模。type Option</t><t> = T | null一个简单的 Option 其实就是一个或类型, 当然你可以使用一个更加复杂的 Option 实现, 不过不在我们今天的讨论范围内。经过修改后的代码变成了这样:

type CreditCard = {  cardNo: string  firstName: string  middleName: Option<string>  lastName: string  contactEmail: Email  contactPhone: Phone}
复制代码

避免基本类型偏执(Primitive Obsession)

问:cardNo可以用 string 来表示吗?如果是,它可以是任意字符串吗?firstName可以是任意长度的字符串吗?很显然,你无法回答上面的问题,源于这个模型并没有包含有此类领域知识。也许在编程语言里面,cardNo可以用 string 表达,但是cardNo在领域模型中,string无法表达出cardNo的领域知识。cardNo是一个200打头的 19 位字符串,name是一个不超过 50 位的字符串,这样的领域信息可以通过type alias来实现:

type CardNo = stringtype Name50 = string...
复制代码

有了上面两个类型,你就有机会通过定义函数的方式,将cardNo业务规则包含在领域模型中。type GetCardNo = (cardNo: string) => CardNo如果用户输入了一个 20 位的字符串,函数GetCardNo返回什么?null?抛出异常?实际上函数式编程语言有比异常更加优雅的 Error handling 方式, 例如 Either Monad 或者 Railway oriented programming。本文虽然不包含这类话题,但至少目前我们可以用 Option 来表示这个函数签名:type GetCardNo = (cardNo: string) => Option<cardno>这个函数类型清晰的表达了整个验证过程,用户输入一个字符串, 返回一个 CardNo 类型,或者空。修改后的领域模型变成了这样:

type CreditCard = {  cardNo: Option<CardNo>  firstName: Name50  middleName: Option<string>  lastName: Name50  contactEmail: Email  contactPhone: Phone}
复制代码

于是,现在的代码拥有跟多的领域知识,丰富的类型还充当了单元测试的角色,例如,你永远都不会把一个 email 赋值给 contactPhone,它们不是 string, 它们代表不同的领域知识。

领域模型的原子性和聚合性

这个领域模型中的三个 name 可以分别修改吗?例如只修改middle name?如果不可以,如何将这种原子性的修改知识包含在领域模型中?实际上我们很容易就能把NameContact两个类型分离出来并加以组合:

type Name = {  firstName: Name50  middleName: Option<string>  lastName: Name50}type Contact = {  contactEmail: Email  contactPhone: Phone}type CreditCard3 = {  cardNo: Option<CardNo>  name: Name  contact: Contact}
复制代码

让错误的状态无法表示

在领域建模过程中,这是一条非常重要的原则,用通俗的话可以理解为:你建立的领域模型应该有尽可能多的静态检查和约束,让错误发生在编译时,而不是运行时,从而杜绝犯错误的机会。其实整个领域建模都是在遵循这个原则,例如上面的 Email 类型和 Phone 类型,为什么不用 string 来表示呢?因为 string 给与的领域知识不够,从而允许开发人员有了犯错误的机会。让我们最后看一个例子,用来说明这条原则如何被应用在领域建模中。上面领域模型中有一个 contact 类型,包含一个 Email 和 Phone 属性。支付成功后,系统可以通过这两个属性给用户发通知,由此延伸出来这样一条规则:用户必须至少填写 Email 或者 Phone 来接受支付消息。首先,上面的领域模型是不匹配这条业务规则的,因为 Email 和 Phone 类型都是非空类型,意味着这两个属性都应该是必填项。我们能不能把它俩都改为 Option 类型呢?

type Contact = {  contactEmail: Option<Email>  contactPhone: Option<Phone>}
复制代码

显然也不行,实际上就是违反了让错误的状态无法表示(Make illegal states unrepresentable), 从而给与了代码犯错的机会,你的领域模型表达出了一种非法的状态,即 Email 和 Phone 都可以为空,你也许会说我的 xxService 做了验证呢,它俩绝对不会同时为空。对不起,我们希望我们的领域模型能够包含这种领域知识,至于 xxService,跟领域模型无关。到底能否将这一规则表达在领域模型中呢?答案是肯定的,规则中有一个“或”字,即我们可以通过Or类型(union)来表达这种关系:

type OnlyContactEmail = Email type OnlyContactPhone = Phonetype BothContactEmailAndPhone = Email & Phonetype Contact =   | OnlyContactEmail  | OnlyContactPhone  | BothContactEmailAndPhone
复制代码

结束语

本文旨在通过函数式编程语言来指导领域建模,整个代码示例中没有出现类或者子类,更不会出现abstract,bean等关键字,衡量一个领域模型的好坏取决于

  1. 领域模型是否包含了尽可能多的领域知识,能否反映领域专家脑海中的业务模型

  2. 领域模型能否成为文档,进而成为所有人沟通和共享知识的途径


同时,一些语言,框架的”行话“应该越少越好,例如你在领域模型中创建了一个叫做AbstractContactBase的类,除了增加复杂度,对共享领域模型这一目的帮助甚少。实际上函数式编程语言的类型系统,不但能够帮助开发者建立一个丰富的领域模型,同时简单可组合的类型系统,也为代码即文档提供了基础。不可否认真实世界远比本文所描述的例子复杂,但是大部分复杂的部分,并不会出现在领域模型中,例如函数式编程中的各种”行话“,他们往往出现在数据请求的 validation, 请求第三方,数据转化,持久化等实现阶段。


本文转载自:ThoughtWorks 洞见(ID:TW-Insights)

原文链接:使用函数式语言建立领域模型

2021-06-03 13:001555

评论

发布
暂无评论
发现更多内容

低代码是在炒概念,炒得不错

Justin

低代码 28天写作

【CSS】文字特效(text-shadow)

德育处主任

CSS css3 html/css CSS小技巧 28天写作

架构师训练营 - 第三周作业

Mark

听过闰年闰月,听过闰秒吗?

艾小仙

后端

如何轻松面过阿里社招Java岗,九大核心模块为你保驾护航!(知识点+面经)

Java架构追梦

Java 架构 面试

胆大无险,脚踏实地

.

28天写作

短视频+直播=最自然的表达方式?| 视频号 28 天 (09)

赵新龙

28天写作

重学JS | 箭头函数为什么不能用做构造函数?

梁龙先森

面试 大前端 编程语言 28天写作

沉寂唯品会3年,毅然辞职冲刺阿里,我是怎么备战金三银四的?

比伯

Java 编程 架构 面试 程序人生

Java虚拟机内存模型

xcbeyond

Java JVM Java虚拟机 28天写作

像用户一样测试:别掉链子

QualityFocus

软件测试 用户体验 回归测试

Soul 网关实践 03|http 请求接入网关

哼干嘛

Java 探索与实践 Soul网关

电商网站商品管理(三)group by+avg+sort等聚合分析

escray

elasticsearch elastic 28天写作 死磕Elasticsearch 60天通过Elastic认证考试

Java 程序经验小结:接口只用于定义类型

后台技术汇

28天写作

深入理解nodejs中的异步编程

程序那些事

多线程 nodejs 异步编程 程序那些事 回调

餐饮品牌资产上链,产业区块链史诗级浪潮已到来

CECBC

餐饮

架构师训练营第八周课后作业

万有引力

想象力,科幻与其他「关于科幻 8/28」

道伟

28天写作

案例研究之聊聊 QLExpress 源码 (八-1)

小诚信驿站

聊聊架构 28天写作 QLExpress源码 聊聊源码

生产环境全链路压测建设历程 29:FAQ 之 评估方法论、改造、痛点

数列科技杨德华

28天写作

智能building之 园区

张老蔫

28天写作

【计算机内功修炼】六:10张图让你彻底理解回调函数

码农的荒岛求生

异步 同步 异步IO 回调函数 异步编程

区块链介绍

v16629866266

区块链

区块链赋能物流行业:“圈子文化”下,网络货运的数字化转型之路

CECBC

区块链 区块链物流

早上听了一场关于财富的线上专题分享

熊斌

财富自由 28天写作

如何通过即构小程序组件实现直播带货功能?

ZEGO即构

小程序 直播带货

2021年“区块链+”将会如何发展?

CECBC

区块链

项目管理系列(3)-如何开好一个项目启动会

Ian哥

项目管理 28天写作

Soul 网关实践 02|选择器&规则介绍

哼干嘛

Java 探索与实践 Soul网关

[6/28]产品业务数据分析的质量实践

L3C老司机

Redis 学习笔记 04:跳跃表

架构精进之路

redis 七日更 28天写作

使用函数式语言建立领域模型_语言 & 开发_张凯峰_InfoQ精选文章