长期以来我都在实践OOP,进而通过OOP来实现DDD,通过面向对象的技巧来建立一个领域模型。OO的一些特性在建立领域模型时显得恰如其分,能否掌握OO的技巧,对创建领域模型有着至关重要的作用。这篇文章为大家介绍一种常见的函数式架构,特别是如何通过函数式语言实现DDD,进而利用函数式组合的特性,创建函数pipeline。软件架构是围绕着领域模型而做的若干设计,如果按照C4模型的定义,软件架构由下面四个级别的架构组成的:
使用单体应用来承载多个限界上下文
领域驱动设计中有一半概念是在讨论问题域,并不是一上来就教你如何写代码,这说明理解一个问题域是复杂的,看清问题的本质是需要时间的。当你开始着手划分限界上下文的时候,说明你已经对需求有了很好的了解。但是经验告诉我们,刚开始你的理解,往往都不是最终的需求,或者仍然需要多次跟领域专家确认和交互,才能得到最终的需求。这个时候,如果你一上来就按照限界上下文划分微服务,往往可能会步入Microservice Premium。要想软件在一开始就能达到快速试错的目的,一上来就做微服务, 会让步子迈得有点大。微服务架构带来了分布式的复杂性,使得前期生产效率大大降低,另外还存在船大难掉头的情况,一旦设计出现返工,生产效率也会打折扣。当然,这不是绝对的,如果架构师已经在该行业深耕多年,对业务更是了如指掌,项目一开始就设计为微服务也未尝不可。在项目初期,在需求还不是非常明确的时候,你完全可以创建一个单体应用,然后通过不同的模块或程序集来隔离不同的界限上下文,通过不断的试错和快速反馈来调整你的解决方案。一种比较严格的说法是,当你关闭其中一个微服务,如果整个应用程序都崩了,其实你设计的不是一个微服务架构,而是一个分布式单体应用程序。
代码结构
在过去的若干年里,我经常使用一种叫“Layer architecture”的软件架构, 这种架构往往把代码分成若干层:
这种架构方式是一个自上往下的输入,最后从下往上输出结果的工作流(图1)
信任边界
在问题域里,各种业务之间的边界是模糊的,限界上下文则是业务在解决方案上的映射,是人为划分的边界。在边界里面的内容,是可信任和合法的,相反,界限外面的一切输入,则是非法和不可信任的(图3)。
这就要求我们在限界上下文的边界,引入验证逻辑,从而阻止外部输入,以及验证对外部的输出。常见的验证逻辑如:
验证逻辑并不是FP独有的,不过FP中常常使用Applicative对数据进行验证,从而收集多个用户Error。关于Applicative, 以后会单独写文章介绍。一旦输入数据突破信任边界,在领域模型建模的过程中,你不需要担心用户名是否是空,邮件格式是否正确等问题。你应该专注于使用FP的代数数据类型进行领域建模,请参考我之前写过一篇使用函数式语言来建立领域模型—类型组合。对输出的验证则不太一样,主要关心对输出数据的安全性保护,防止将一些领域模型中的私有属性输出到外部世界。
通过状态机来处理业务逻辑
纵然,通过FP的代数数据类型(Algebraic data type)能够快速完成领域建模,但是我们知道,领域模型不是静态的,它是由一些列事件组成的过程。而这种转化过程,正是领域模型状态发生变化的过程,即状态机(图4)。
领域模型状态转换的过程跟实现语言无关,一个设计精良的领域模型,就好比一个状态机。例如在买机票的过程中,填写个人信息,填写联系人,选座,买保险和付款的过程,就是订单状态发生变化的过程。再比如用户注册的过程,填写基本信息,验证邮箱,也是用户信息状态发生变化的过程。以OO为例,我们习惯于通过增加标志位的方式,进行领域建模:
type User = {
name: string
password: string
email: Email | null
isEmailVerified: boolean //当验证完email后设置为true
canLogin: boolean //当email被验证后方可login
}
业务逻辑的实现过程,就是填充用户属性和修改标志位的过程。然而,这种方式实际上存在若干问题:
通过状态机的机制,重新考虑用户注册过程:(图5)
type UnVerifiedUser = {
name: string
password: string
}
type VerifiedEmailUser = {
name: string
password: string
email: Email
}
type User =
| UnVerifiedUser
| VerifiedEmailUser
如果有更多的用户状态,你还可以持续添加到User类型中。这种通过”|”创建的User类型被称为在FP中被称为union类型,也叫product或sum类型, 在TypeScript被称为Discriminated union。这时候的User类型,可以用来在领域模型中实现领域逻辑,通常这种union类型需要配合模式匹配来完成,例如修改密码,登录,修改邮件地址等逻辑,都是针对User类型做模式匹配的过程。关于模式匹配的用法,在此不再细说。这种通过状态机的方式,实现业务逻辑时有下面几个好处:
保持纯净的领域模型
函数式编程的一个主要目标就是让代码有预测性,通过函数签名理解函数的用途。为了达到这个目的,函数式语言设计了若干特性,例如不可变的数据结构,还有各类Monad来避免副作用。在DDD实践中,应该避免I/O相关的代码出现Domain中。例如读写数据库,调用第三方系统的API等相关代码,需要把这类具有副作用的代码推到Domain的外围。如果需要做的更好,那就必须使用CQRS加Event Sourcing。我在之前一篇文章提到过这个观点,不过部分读者没有理解其中的意思,我在这里再做一些说明。首先,CQRS不仅仅是为了读写分离,从而提高读写性能。读模型和写模型(领域模型)的分离意味着职责也是分离的,从而在设计领域模型的时候,打消对查询性能的考虑,有助于设计出纯净的领域模型。当然仅靠CQRS还是不够的,有些时候任然无法完全脱离数据库的考虑,因为领域模型始终是要持久化在数据库里,你就要考虑数据库相关的约束,例如主外键,如何建表,如何高效存储一个列表等。而持久化一个Event则完全摆脱了数据库技术,因为一个Event就是一个json, 只有这样才能设计出理想的领域模型。当然引入CQRS和ES在项目初期成本略高,不再详细描述。
通过Monad创建pipeline
以API为例,一个完整的用户请求就是一个Pipeline(图6)。假设每一步都是有若干个函数组成,我们能够将他们组合到一起吗?答案是很难,主要原因如下:
而解决这一切的手段就是Monad, 简而言之,Monad是一种抽象方式,能够将monadic风格的函数连接起来。什么又是monadic? 简单来说这是一种接收普通类型,返回某种lift类型(泛型)的函数。例如通过IO, Task, Either相关的Monad来解决此类问题。具体内容请关注本人的函数式系列博客。
小结
这篇文章总结了一些使用函数式语言实践DDD的大致思路,也为函数式架构提供了一些参考。由于篇幅的原因,并没有介绍到DDD的方方面面,同时,一些实现细节则是点到为止,例如如何使用Monad。总体来说,函数式语言的代数数据类型,以及函数式的一些思想,为实践领域驱动设计提供了其他的选择。
本文分享自 ThoughtWorks洞见 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!