长期以来我都在实践OOP,进而通过OOP来实现DDD,特别是如何通过面向对象的技巧来建立一个领域模型。OO的一些特性在建立领域模型时显得恰如其分,能否掌握OO的技巧,对创建领域模型有着至关重要的作用。

这篇文章为大家介绍一种常见的函数式架构,特别是如何通过函数式语言来实现DDD,进而利用函数式组合的特性,创建函数pipeline。

软件架构是围绕着领域模型而做的若干设计,如果按照c4模型的定义,软件架构由下面四个级别的架构组成的:

  • "System context"是最高层的架构,代表着整个系统
  • "Container"是组成"System context"的单元,通常用来表示可部署的单元,例如一个"API service", 一个web应用程序等
  • "Component"是组成"Container"的基本单元,通常指组若干抽象组件,是一个"Container"里面的骨架,也是本文要重点介绍的架构
  • "Code"具体到了代码级别,通常指实现某个"Component"应该有哪几个类组成

使用单体应用来承载多个限界上下文

领域驱动设计中有一半概念是在讨论问题域,并不是一上来就教你如何写代码,这说明理解一个问题域是复杂的,看清问题的本质是需要时间的。当你开始着手划分限界上下文的时候,说明你已经对需求有了很好的了解。但是经验告诉我们,刚开始你的理解,往往都不是最终的需求,或者仍然需要多次跟领域专家确认和交互,才能得到最终的需求。

这个时候,如果你一上来就按照限界上下文划分微服务,往往可能会步入Microservice Premium

要想软件在一开始就能达到快速试错的目的,一上来就做微服务, 会让步子迈得有点大。微服务架构带来了分布式的复杂性,使得前期生产效率大大降低,另外还存在船大难掉头的情况,一旦设计出现返工,生产效率也会打折扣(图一)。当然,这不是绝对的,如果架构师已经在该行业深耕多年,对业务更是了如指掌,项目一开始就设计为微服务也未尝不可。

在项目初期,在需求还不是非常明确的时候,你完全可以创建一个单体应用,然后通过不同的模块或程序集来隔离不同的界限上下文,通过不断的试错和快速反馈来调整你的解决方案。

一种比较严格的说法是,当你关闭其中一个微服务,如果整个应用程序都崩了,其实你设计的不是一个微服务架构,而是一个分布式单体应用程序。

代码结构

在过去的若干年里,我经常使用一种叫“Layer architecture"的软件架构, 这种架构往往把代码分成若干层:

  • 基础设施层:通常用来负责跟第三方或者数据库打交道,用来持久化数据或者API请求。
  • 领域层或者业务逻辑层:用来封装业务逻辑
  • 应用程序层:通常是很薄的一层,用来协调领域层和基础设施层
  • 展现层:用来展现UI或者输出API结果

    这种架构方式是一个自上往下的输入,最后从下往上输出结果的工作流(图1)。



    实际上,当我在使用这种方式组织代码时,遇到最大的挑战在于:这种分层方式,把同一个输入到输出的的若干部分,横向的分散到了若干层中。当你需要修改某个API时,需要同时修改若干个层。另外这种组织代码的方式,往往会让OO走向混乱,一个名叫OrderApplicationService的类中放满了各种跟Order相关的方法,通常对Order的操作有数十种之多,他们属于OrderApplicationService吗?如果属于,任何一个跟Order相关操作的参数变化,都会引起这个类被改动,这种对类的频繁修改合理吗?

    函数式编程中,更倾向于纵向组织代码(图2),



    例如一个API操作,就是一个文件或者模块,整个操作自上而下的流程被组织到同一个文件里,这样做的好处是,针对某个功能的修改,只关注与当前工作流相关的文件即可。

信任边界

在问题域里,各种业务之间的边界是模糊的,限界上下文则是业务在解决方案上的映射,是人为划分的边界。在边界里面的内容,是可信任和合法的,相反,界限外面的一切输入,则是非法和不可信任的(图3)。



这就要求我们在限界上下文的边界,引入验证逻辑,从而阻止外部输入,以及验证对外部的输出。

常见的验证逻辑如:

  • 输入DTO,需要转化为领域模型,用于处理业务逻辑
  • 对输入数据的合法性验证,例如:用户名不能为空,邮件格式是否正确
  • 对输出类型的安全性校验,例如:防止在输出数据里包含用户密码等敏感信息

    验证逻辑并不是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
}

业务逻辑的实现过程,就是填充用户属性和修改标志位的过程。然而,这种方式实际上存在若干问题:

  • 有些属性在业务前期是不需要的,例如canLogin, 只有验证完email才有效
  • 有些标志位实际上不是单独存在的,例如isPhoneVerified就跟phone是紧密相关的,而这个模型无法反映出来这一信息
  • phone和email被定义为可空类型,导致使用该模型的地方不得不使用null检查

    通过状态机的机制,重新考虑用户注册过程:(图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)。



假设每一步都是有若干个函数组成,我们能够将他们组合到一起吗?答案是很难,主要原因如下:

  • 每一步的若干个函数签名很难保持一致,导致compose这样的函数无法正常工作
  • 部分I/O相关的函数可能是异步的,领域模型中的代码大多是同步的,很难将他们组合在一起
  • 在函数式编程中,通常不会通过try...catch的方式处理异常,一方面异常也是一种副作用,另一方面,异常让函数签名不再完整。如何把每一步的异常带到最外面也成了问题

    而解决这一切的手段就是Monad, 简而言之,Monad是一种抽象方式,能够将monadic风格的函数连接起来。什么又是monadic? 简单来说这是一种接收普通类型,返回某种lift类型(泛型)的函数。例如通过IO, Task, Either相关的Monad来解决此类问题。具体内容请关注本人的函数式系列博客。

小结

这篇文章总结了一些使用函数式语言实践DDD的大致思路,也为函数式架构提供了一些参考。由于篇幅的原因,并没有介绍到DDD的方方面面,同时,一些实现细节则是点到为止,例如如何使用Monad。总体来说,函数式语言的代数数据类型,以及函数式的一些思想,为实践领域驱动设计提供了其他的选

使用函数式语言实践DDD的更多相关文章

  1. 《程序设计语言——实践之路》【PDF】下载

    程序设计语言--实践之路>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230382240 内容简介 本书在美国大学已有使用了十余年,目前被欧 ...

  2. 《程序设计语言——实践之路(英文第三版)》【PDF】下载

    <程序设计语言--实践之路(英文第三版)>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230382234 内容简介 <程序设计语 ...

  3. 《程序设计语言——实践之路【PDF】下载

    <程序设计语言--实践之路[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230382240 内容简介 <程序设计语言--实践之路(第3版 ...

  4. R语言︱H2o深度学习的一些R语言实践——H2o包

    每每以为攀得众山小,可.每每又切实来到起点,大牛们,缓缓脚步来俺笔记葩分享一下吧,please~ --------------------------- R语言H2o包的几个应用案例 笔者寄语:受启发 ...

  5. 关于Function Language(函数式语言是什么?包含哪些语言?为什么函数式语言流行?)

    1.What? Function Language是一种非冯诺依曼式的程序设计语言.函数式语言的主要成分是原始函数.定义函数和函数型. 这种语言具有较强的组织数据结构的能力,可以把某一数据结构(如数组 ...

  6. 函数式语言简介(functional language)

    1.什么是函数式语言?        是一种非冯·诺伊曼式的程序设计语言.函数式语言主要成分是原始函数.定义函数和函数型.这种语言具有较强的组织数据结构的能力,可以把某一数据结构(如数组)作为单一值处 ...

  7. 函数式语言(Functional language)简单介绍

    函数式语言(functional language)一类程序设计语言,是一种非冯·诺伊曼式的程序设计语言.函数式语言主要成分是原始函数.定义函数和函数型. 函数式语言有:Haskell,Clean,M ...

  8. Atitit.编程语言的主要的种类and趋势 逻辑式语言..函数式语言...命令式语言

    Atitit.编程语言的主要的种类and趋势 逻辑式语言..函数式语言...命令式语言 1. 编程语言的主要的种类 逻辑式语言..函数式语言...命令式语言 1 2. 逻辑式语言,,不必考虑实现过程而 ...

  9. 函数式语言(functional language)定义、函数式语言的种类以及为什么函数式语言会流行起来的学习笔记

    一.什么是函数式语言?       函数式语言一类程序设计语言,是一种非冯·诺伊曼式的程序设计语言.函数式语言主要成分是原始函数.定义函数和函数型.这种语言具有较强的组织数据结构的能力,可以把某一数据 ...

随机推荐

  1. 六、Python集合定义和基本操作方法

    一.集合的定义方法及特点 1.特点: (1)由不同元素组成 #集合由不同元素构成 s={1,2,3,3,4,3,3,} print(s)#运行结果:{1, 2, 3, 4} (2)集合无序 #集合无序 ...

  2. python Crypto 加密解密

    本片文字记录使用python 的Crypto 工具对图片或者文本进行加密解密的方法: import numpy as np from PIL import Image from base64 impo ...

  3. Pymongo 笔记

    Pymongo 1.MongoDB概念 MongoDB是一种非关系型数据库(NoSQL),MongoDB数据存储于内存,内存不足则将热度低数据写回磁盘.存储的数据结构为文档.每个数据库包含若干集合(c ...

  4. chrome禁用缓存:调试javascript注意事项

    chrome禁用缓存:调试javascript   chrome对js和图片的缓存,导致调试的程序不是最新的,有时F5刷新了都没用. 可以禁用缓存: 先按F12,再按F1, 勾选 Disable ca ...

  5. css position sticky All In One

    css position sticky All In One css sticky & 吸顶效果 demo https://codepen.io/xgqfrms/pen/PoqyVYz ref ...

  6. webpack 5 模块联合

    webpack 5 模块联合 webpack 5 https://webpack.docschina.org/concepts/module-federation/ https://github.co ...

  7. how to recursively all files in a folder with sudo permissions in macOS

    how to recursively all files in a folder with sudo permissions in macOS write bug OK sudo chmod 777 ...

  8. TS & error

    TS & error Function implementation is missing or not immediately following the declaration.ts ht ...

  9. rxjs 常用的subject

    api列表 Subject Subject是可观察的一种特殊类型,它允许将值多播到许多观察者 import {Subject} from 'rxjs'; const l = console.log; ...

  10. .Net Core 3.1浏览器后端服务(三) Swagger引入与应用

    一.前言 前后端分离的软件开发方式已逐步成为互联网项目开发的业界标准,前后端分离带来了诸多好处的同时,也带来了一些弊端. 接口文档的维护就是其中之一,起初前后端约定文档规范,开发的很愉快,随着时间推移 ...