本文是函数式编程思想与领域建模的第三部分,是对本主题的一次总结。
遵循函数范式建立领域模型时,代数数据类型与纯函数是主要的建模元素。代数数据类型中的和类型与积类型可以表达领域概念,纯函数则用于表达领域行为。它们都被定义为不变的原子类型,然后再将这些原子的类型与操作组合起来,满足复杂业务逻辑的需要。这是函数式编程中面向组合子(combinator)的建模方法,它与面向对象的建模方法存在思想上的不同。
面向对象的建模方法是一种归纳法,通过分析和归纳需求,找到问题域并逐级分解问题,然后通过对象来表达领域逻辑,并以职责的角度分析这些领域逻辑,按照角色把职责分配给各自的对象,通过对象之间的协作实现复杂的领域行为。
面向组合子的建模方法则是一种演绎法,通过在领域需求中寻找和定义最基本的原子操作,然后根据基本的组合规则将这些原子类型与原子函数组合起来。
因此,函数范式对领域建模的影响是全方位的,它与对象范式看待世界的角度迥然不同。
对象范式是在定义一个完整的世界,然后以上帝的身份去规划各自行使职责的对象;
函数范式是在组合一个完整的世界,它就像古代哲学家一般,看透了物质的本原而识别出不可再分的原子微粒,然后再按照期望的方式组合这些微粒来创造世界。
故而,采用函数范式进行领域建模,关键是组合子包括组合规则的设计,既要简单,又要完整,还需要保证每个组合子的正交性,如此才能对其进行组合,互不冗余,互不干涉。这些组合子,就是前面介绍的代数数据类型和纯函数。
通过前面给出的案例,我们发现函数范式的领域模型颠覆了面向对象思想中“贫血模型是坏的”这一观点。事实上,函数范式的贫血模型不同于结构范式和对象范式的贫血模型。
结构范式是将过程与数据分离,这些过程实现的是一个完整的业务场景,由于缺乏完整的封装性,因而无法控制对过程与数据的修改对其他调用者带来的影响。
对象范式要求将数据与行为封装在一起,就是为了解决这一问题。
函数范式虽然建立的是贫血模型,但它的模块化、抽象化与可组合特征降低了变化带来的影响。在组合这些组合子时,通过引入高内聚松耦合的模块对这些功能进行分组,就能避免细粒度的组合子过于散乱,形成更加清晰的代码层次。
Debasish Ghosh总结了函数范式的基本原则,用以建立更好的领域模型:
- 利用函数组合的力量,用小函数组装成一个大函数,获得更好的组合性。
- 纯粹,领域模型的很多部分都由引用透明的表达式组成。
- 通过方程式推导,可以很容易地推导和验证领域行为。
不止如此,根据代数数据类型的不变性以及对模式匹配的支持,它还天生适合表达领域事件。例如地址变更事件,就可以用一个积类型来表示:
case class AddressChanged(eventId: EventId, customerId: CustomerId, oldAddress: Address, newAddress: Address, occurred: Time)
我们还可以用和类型对事件进行抽象,这样就可以在处理事件时运用模式匹配:
sealed trait Event { def eventId: EventId def occurred: Time } case class AddressChanged(eventId: EventId, customerId: CustomerId, oldAddress: Address, newAddress: Address, occurred: Time) extends Event case class AccountOpened(eventId: EventId, Account: Account, occurred: Time) extends Event def handle(event: Event) = event match { case ac: AddressChanged => ... case ao: AccountOpened => ... }
函数范式中的代数数据类型仍然可以用来表示实体和值对象,但它们都是不变的,二者的区别主要在于是否需要定义唯一标识符。
聚合的概念仍然存在,如果使用Scala语言,往往会为聚合定义满足角色特征的trait,这样就可以使得聚合的实现通过混入多个trait来完成代数数据类型的组合。
由于资源库(Repository)会与外部资源进行协作,意味着它会产生副作用,因此遵循函数式编程思想,往往会将其推向纯函数的外部。在函数式语言中,可以利用柯里化(Currying,又译作咖喱化)或者Reader Monad来推迟对资源库具体实现的注入。
主流的领域驱动设计往往以对象范式作为建模范式,利用函数范式建立的领域模型多多少少显得有点“另类”,因此我将其称之为非主流的领域驱动设计。
这里所谓的“非主流”,仅仅是从建模范式的普及性角度来考虑的,并不能说明二者的优劣与高下之分。事实上,函数范式可以很好地与事件驱动架构结合在一起,这是一种以领域事件作为模型驱动设计的驱动力思想。
针对事件进行建模,则任何业务流程皆可用状态机来表达。状态的迁移,就是命令(command)或者决策(decision)对事件的触发。我们还可以利用事件风暴(Event Storming)帮助我们识别这些事件,而事件的不变性特征又可以很好地与函数式编程结合起来。
如果采用命令查询职责分离(CQRS)模式,那么在命令端,将由命令与事件组成一系列异步的非阻塞消息流。这种对消息的认识,恰好可以与响应式编程(Reactive Programming)结合起来。诸如ReactiveX这样的响应式编程框架在参考了迭代器模式与观察者模式的基础上,结合了函数式编程思想,以事件处理的形式实现了异步非阻塞处理,在满足系统架构灵活性与伸缩性的同时,提高了事件处理的响应能力。
显然,围绕着不变的事件为中心,包括响应式编程、事件风暴、事件溯源与命令查询职责分离模式都可以与函数范式有效地结合起来,形成一种事件模型驱动设计(Event Model Driven Design, EDDD)方法。与事件驱动架构不同,事件模型驱动设计可以算是领域驱动设计的一种分支。
作为一种设计方法学,它的实践与模式同样涵盖了战略设计与战术设计等多个层次,且可以与领域驱动设计的模式如限界上下文、领域事件、领域服务等结合起来。在金融、通信等少数领域,已经开始了对这种建立在函数范式基础之上的领域驱动设计的尝试,与它们相关的知识可以写成厚厚的一本大书,在这里就不再赘述了。