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

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

领域模型=代码=文档

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

那么DDD则为敏捷开发过程当中的沟通形式做出了进一步的补充,DDD让领域模型和代码以及文档之间画上了等号,主张让代码成为团队之间沟通和交流的途径。纵观DDD的全部环节,无一不是在打通领域专家和开发人员之间的沟通和交流,而代码无疑是最有效,最实时的共享模型。
DDD的精髓在于经过让开发人员理解领域,进而让开发人员使用编程语言创建一个跟领域专家脑海中一致的领域模型,使得该领域模型成为你们共享知识的途径,这将有效的减小不一样利益相关者的沟通及交流,确保全部人都在解决同一个问题。
git

领域建模

领域建模是整个DDD环节中最最考验开发人员功底的一环,不一样于传统的数据库建模技术,开发人员须要有很好的抽象能力,经过恰如其分的编程技术,将领域知识映射到一个代码模型中。
长期以来OO语言被认为是领域建模的首选,一些OO的技巧能够很好的用来抽象领域模型。而函数式语言则被广泛认为只能用来作数据处理,科学计算等。本文将为你们展现如何经过函数式编程语言进行领域建模,本文选用TypeScript编写实例,TypeScript类型系统彻底知足函数式编程需求,固然本文也适用于其余拥有静态类型系统的函数式编程语言。github

TypeScript的类型系统

实际上你只须要知道少许的知识就能够开始领域建模了,从这个角度来说,实际上函数式类型系统更适合领域建模,从而让领域模型成为文档。typescript

类型

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

type Name = {
  firstName: string
  middleName: string
  lastName: string
}

上面类型的用途是显而易见的,除此以外type还有起别名的用途,不要小瞧这个特性,他能够帮助你把领域知识记载在你的领域模型中,考虑下面的代码:编程

const timeToFly = 10

你能一眼看出这句代码表明的领域知识吗?也许不能,fly多久?查文档?No,你应该时刻告诉本身,代码等于文档。改进后的代码以下:session

type Second = number
const timeToFly: Second = 10

Or类型

OO语言没法建立这种类型,在TypeScript,这种类型被称为联合(Union Types),经过符号|来建立,考虑下面的类型:框架

type Pet = Fish | Bird

PetFish或者是Bird类型。通常来讲函数式语言都会有强大的模式匹配能力,来处理这种类型,然而受制于TypesScript没有模式匹配或者说能力很弱,一般状况下,会在类型里面添加一个字符串字面量, 从而来区分不一样的类型, 在次再也不细说。编程语言

And类型

在Typescript中,这种类型被称为交叉类型(Intersection Types),经过符号&来建立,考虑下面的类型:函数式编程

type ABC = A & B & C

表示ABC类型包含全部A、B、C三个类型里面的属性。

定义函数类型

在TypeScript中,函数与其余类型没什么区别,也能够经过type关键字来定义,例如:

type Add = (a: number) => (b: number) => number

Add是一个函数,接收两个类型为number的类型a和b,返回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 | 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 = string
type 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
}

Make illegal states unrepresentable

在领域建模过程当中,这是一条很是重要的原则,用通俗的话能够理解为:你创建的领域模型应该有尽量多的静态检查和约束,让错误发生在编译时,而不是运行时,从而杜绝犯错误的机会。其实整个领域建模都是在遵循这个原则,例如上面的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 = Phone
type BothContactEmailAndPhone = Email & Phone

type Contact = 
  | OnlyContactEmail
  | OnlyContactPhone
  | BothContactEmailAndPhone

结束语

本文旨在经过函数式编程语言来指导领域建模,整个代码示例中没有出现类或者子类,更不会出现abstract, bean等关键字,衡量一个领域模型的好坏取决于
1)领域模型是否包含了尽量多的领域知识,可否反映领域专家脑海中的业务模型
2)领域模型可否成为文档,进而成为全部人沟通和共享知识的途径
同时,一些语言,框架的”行话“应该越少越好,例如你在领域模型中建立了一个叫作AbstractContactBase的类,除了增长复杂度,对共享领域模型这一目的帮助甚少。 实际上函数式编程语言的类型系统,不但可以帮助开发者创建一个丰富的领域模型,同时简单可组合的类型系统,也为代码即文档提供了基础。不能否认真实世界远比本文所描述的例子复杂,可是大部分复杂的部分,并不会出如今领域模型中,例如函数式编程中的各类”行话“,他们每每出如今数据请求的validation, 请求第三方,数据转化,持久化等实现阶段。在将来的文章中将会描述整个http请求到领域模型再到输出过程当中如何经过函数式编程语言来实现。