发布在即!企业 AIGC 应用程度测评,3 步定制专属评估报告。抢首批测评权益>>> 了解详情
写点什么

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

  • 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:001131

评论

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

面试官:你工作了3年了,这道算法题你都答不出来?

高端章鱼哥

面试 算法 LeetCode

区块链数字货币交易所开发方案,交易平台搭建

V\TG【ch3nguang】

代码检查过程中为什么需要涉及到编译呢?

华为云开发者联盟

开发 华为云 华为云开发者联盟 代码检查 企业号9月PK榜

PDF Squeezer mac(pdf文档大小压缩软件) v4.3.7永久激活版

mac

苹果mac Windows软件 PDF Squeezer 文件压缩软件

企业即时通讯解决方案,WorkPlus助力高效沟通与协作

WorkPlus

企业即时通讯

JDK的配置验证

小齐写代码

低功耗引擎Cliptrix有什么价值

Onegun

物联网 IoT

支持私有化部署的WorkPlus,助您构建定制化的即时通讯平台

WorkPlus

1周开发上线“中医舌诊”元服务,5天吸引超2万付费用户

最新动态

DEFI/DApp/DAO/IDO/LP子母币/单双币/机枪池流动性代币质押项目挖矿系统开发

l8l259l3365

龙蜥社区与您相约 2023 KubeCon

OpenAnolis小助手

开源 容器 云原生 龙蜥社区 kata

2000字说清音视频在社交娱乐场景中的应用!

X2Rtc

音视频 RTC 社交娱乐 融合通信

Mate 60系列搭载方舟引擎,华为游戏中心解锁飞驰游戏体验

最新动态

软件测试/测试开发丨探索AI与测试报告的完美结合,提升工作效率

测试人

人工智能 程序员 软件测试 ChatGPT

优化模型之标注错误篇

矩视智能

深度学习 工业机器视觉

腾讯云数据库再获顶会认可,论文入选VLDB2023

Geek_2d6073

Sovit2D组态设计 Web Scada烟气脱硫工艺流程

2D3D前端可视化开发

物联网 组态软件 工业控制 工业自动化 烟气脱硫

DeFi 集成:扩大加密货币交易平台开发的视野

区块链软件开发推广运营

交易所开发 dapp开发 链游开发 NFT开发 区块拉开发

用智能文字识别技术赋能古彝文数字化之路

dvlinker

人工智能 深度学习 合合信息 古彝文 智能文字识别技术

聚焦华为全联接大会:和鲸科技与华为携手助推交叉学科应用型数据科学人才培养

ModelWhale

华为 人才培养 数据科学 高等教育 交叉学科

根据人类反馈进行强化学习的5大关键步骤

澳鹏Appen

大模型 数据训练 生成式AI LLM RLHF

软件测试 | AI大模型应用开发实训营来啦~ 大模型学习资料免费领

测试人

人工智能 程序员 AI 软件测试 大数据模型

一步教会你如何获取1688商品详情

Noah

API 开发

亮相数字科技出海峰会,火山引擎边缘云助力数字化出海“加速度”

火山引擎边缘云

CDN 加速 火山引擎 内容分发 火山引擎边缘计算

四问复合索引,让你的数据查询速度飞起

华为云开发者联盟

后端 华为云 图数据库 华为云开发者联盟 企业号9月PK榜

SQL还是NoSQL?架构师必备选型技能

树上有只程序猿

nosql sql 业务

Java第一个程序——Hello,World!

小齐写代码

应用架构的演进:亚马逊的微服务实践

亚马逊云科技 (Amazon Web Services)

Serverless DevOps 微服务

软件测试/测试开发丨利用人工智能自动找Bug

测试人

人工智能 程序员 软件测试 bug ChatGPT

适合企业的跨隔离网文件摆渡的四种方式及优缺点

镭速

跨网文件交换 跨隔离网文件摆渡

一文了解企业如何实现文件自动化实时同步

镭速

文件同步

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