DDD 落地的一些心得

背景

下文将会描述一些笔者在领域服务实体建模的一些心得,在此之前,如果对于 DDD 没有了解的读者可能会对一些名词迷惑不解。关于我们团队的 DDD 落地实践 可以从以下文章了解。

https://www.jianshu.com/p/7b5d8dc565d3

四色建模法

领域模型是描述业务用例实现的对象模型。它是对业务角色和业务实体之间应该如何联系和写作以执行业务的一种抽象。
当我们要将 DDD 落地时,可能是最简单且实用的方法就是 四色建模法。假设现在你要为公司开发一套 敏捷项目管理系统 。首先了解一下敏捷的全生命周期:
在这里插入图片描述

根据敏捷的全生命周期,我们可以大致得知开发 敏捷项目管理系统 的核心流程大致是这样的:

核心流程

第一步:寻找要追溯的事件
1.谁,在什么时候,规划了需求
2.谁,在什么时候,分配了任务

第二步:识别 “时标对象”
按时间发展的先后顺序,用红色所表示的起到 “追溯单据” 作用的 “时标” 概念,如下图所示:

第二步

第三步:寻找时标对象周围的 “人、地、物”
在 “时标”对象周围的用绿色所表示的 “人、地、物” 概念,如下图所示:

第三步

第四步:抽象 “角色” [6]
在上图中插入用黄色所表示的 “角色” 概念,如下图所示:

第四步

第五步:补充 “描述” 信息
在上图中插入用蓝色所表示的 “描述” 概念,如下图所示:

第五步

好的,基本的聚合根(在我们的实践中,我们直接将实体作为聚合根)已经出来了,如下图所示:

基本的聚合根

可能用到的设计模式

这时小胡同学已经把聚合根里面关于需求、任务、缺陷的主要的代码都写好了:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

噫~读者朋友有没有发现,这三个聚合根都同时带着 listAssigneeIds() 这个方法 ,那么有没有什么方法可以优化一下捏?

最简单的方式即是 新建一个父类,在父类实现这个 listAssigneeIds 方法,随即将 Task、Requirement、Bug 都继承这个父类。这时,小胡同学脑子灵光一闪,噫,effect java 里面不是有一个原则:——优先选择复合而不是继承吗?

在这里插入图片描述

好的,经过小胡同学改造之后的代码结构是这样的:
在这里插入图片描述

代码是这样的:
在这里插入图片描述

AssignableWrapper 类被称为包装类,因为每个 AssignableWrapper 实例都包含(「包装」) Task、Requirement、Bug 实例。这也称为装饰者模式 ,因为 AssignableWrapper 类通过添加插装来「装饰」Task、Requirement、Bug。
Task、Requirement、Bug 都继承 AssignableWrapper ,然后所有接口如 Assignable 都由 AssignableWrapper 来实现。同时以后想要拓展能力就只需要在 AssignableWrapper 上去实现,同时还避免了 「当在父类中新建一个方法,而子类中忘记重写」这一情况的发生。

小胡同学想到这不禁为自己的聪明才智折服,噼里啪啦一顿操作提交代码给领导 review:

好吧,小胡把 AssignableWrapper 改为 ScrumConcept 总算把代码合进主干了

函数式建模

在使用类似 JAVA 语言的面向对象开发中,我们会用接口来做 API 设计,它将最终把模型的规则发布给终端用户。当准备好接口之后,就可以开始类和对象的具体实现。首先定义类,然后加入一些操作作为这个类的方法。在函数式编程中,将这个过程颠倒了一下——首先定义对应到基本领域行为的操作,然后将相关的操作组合起来,形成更大的用例。

先来一段代码,抛砖引玉:
在这里插入图片描述

Try 是什么?andThen 是什么?我是谁?

别着急朋友们,接下来我会好好的解释的。在读者朋友们继续了解函数式建模之前,请允许我交代一些关于函数式编程的基本知识:

什么是 FP

FP 又称为函数式编程,它是一种以数学函数为编程语言建模的核心编程范式,它将计算机视为数学函数计算;它是一种设计、编制和调试程序的技术,函数式程序是由一些原始函数、定义函数和函数类型的函数表达式组成,函数式编程最初起源于古老的 LISP 语言,而现代函数式编程语言的代表则有 Haskell、Clean、Erlang 以及 Clojure 等。

Monad

Monad与其说是函数式编程的一种特性,不如说它是一种设计模式,将一个运算过程,通过函数式编程拆解成相互连接的多个步骤,只需要提供下一步运算所需要的函数,整个运算就会自动进行下去,当然包括处理整个函数式编程过程中的副作用。

Monad 称为单子,它是一种将函数组合成应用的方法,在计算机科学中,单子用来代表计算,它能用来把业务无关的通用程序抽象出来,比如用来处理并行(Future)、异常(Option/Try)、甚至副作用的单子。

在上文中出现的 代码 4-1-1 示例中,有一个名为 Try 的单子,严格意义上来说,Option 和 Try 不是数据类型,它们是类型构造器,Try 抽象了失败的作用,而 Option 抽象了可选的作用。在我看来,Try 不仅仅是一个处理异常的手段,同时还提供了 抽象能力,这样我们就可以在赋值之前进行组合,建立更大粒度的抽象来应对失败。

当然,在原生的 Java 语言里面是没有 Try 这个概念的 。其来自 scala ,本文出现的 Try 需要引入 Vavr 这一 Java 函数式增强框架。而 NoneParamConsumer 是我自己构建的一个没有返回值单子

高阶函数

函数式语法中很重要的一个特性则是高阶函数(Higher-Order Function),接触高阶函数之前,读者需要先区分几个基本概念:

  • 函数定义: 顾名思义就是定义一个函数,常见的方式是 function xxx,上述代码中,前三个行等号右边部分是函数定义,如:x -> y -> z -> x.apply(y.apply(z)),x -> x * 3,x -> x * x,这些都是函数定义。
  • 函数引用: 函数引用表示一个变量(Java语言中的左值),该变量本身不是一个确切的值,而是一个函数,如上边的:compose,triple,square 以及 f,在 Java 语言中,它不能直接通过 compose() 这种带 () 的方式进行调用,必须使用 apply 方法实现函数调用。
  • 函数调用: 上述代码中,只要出现了 apply 的调用,则是函数调用的代码,这个概念 Java 比 JavaScript 易懂,如果是 JavaScript 你将会看到类似:compose(square)(triple) 的写法。

上边概念搞清楚后,高阶函数的概念就不言而喻了,回到 Java 8:

Function<Function<Integer, Integer>, 
       Function<Function<Integer, Integer>,
                Function<Integer, Integer>>> compose =
                         x -> y -> z -> x.apply(y.apply(z));
Function<Integer, Integer> triple = x -> x * 3;
Function<Integer, Integer> square = x -> x * x;
Function<Integer, Integer> f = compose.apply(square).apply(triple);
System.out.println(f.apply(2));

运行上述代码可以得到36的输出值,

回到正题,上述代码中的 compose 就是高阶函数,一个高阶函数至少满足以下一个条件:

  • 接受一个或多个函数作为输入
  • 返回值是一个函数

而从 compose 的定义可以看到,它的参数是一个函数,返回值是一个高阶函数,该高阶函数的形参和返回值依旧是一个函数,这也是上述代码中 compose 可以直接使用三次 apply 的原因,完整写法应该是:

compose.apply(square).apply(triple).apply(2);

简单来讲,高阶函数在函数定义时可使用函数作为形参,在函数调用后的返回值也是一个函数。

组合抽象

介绍完 Monad 以及 高阶函数 之后,读者朋友可能对 andThen 还是不理解,在这里我要先介绍一下 【kleisli 模式】

Kleisli 组合法则 (Kleisli composition)

Monad 的组合性操作,如一种运算 map,它满足—— map(x, map(y, z)) == map(map(x,y),z),如果不对 Monad的值进行组合操作,而是将 map 演变成 f(x) -> y 的一种证明,那么理解就简单很多,因为 map 本身是一种运算,可写成函数。也就是说如果实现一个 compose 方法,那么可有:compose(x, compose(y, z)) == compose( compose(x,y), z)。

代码 4-1-1 示例中的 andThen 实际上也是符合 Kleisli 组合法则的,使用其可将 deleteItSelf、deleteRequirementByParentId、deleteTaskByRequirementId 组合在一起。

除了 andThencompose 以外,函数组合的方式还有 currying、Lifting、Partial application 等方式。碍于文章边幅,本文不会过多讲解关于 FP 的相关知识,而是将核心思想讲述给读者朋友们。

设计函数式领域模型的通用原则

  • 将不可变状态建模为代数数据类型 (ADT)—— 例如,一个类型由其他类型组合而成。常见的代数类型是 和(sum)类型、积(product)类型。ADT 是类型安全的。
  • 在模块中将行为建模为函数,比如一个领域服务,行为比状态更好组合,因此,在模块中包含相关的行为有助于提升组合性。

什么是代数?

在函数式领域模型中定义的模块是函数的集合,它们操作一系列的类型,遵从一系列被称为领域规则的不变式。用数学的术语来说,这就是模块的代数。

为领域服务定义代数

前文提过,与 OO 基于类的方式不同,模块中首先要有操作,才能使核心领域对象保持瘦小和扁平。行为不再和对象捆绑在一起,同时还可以通过将对象的类型作为它们所代表的代数内容来独立地演变行为。

局部用函数,全局用 OO

在使用 Java8 没有模块的情况下,我建议建模的时候还是按照面向对象的方式进行建模,在聚合根通过 单子 将其领域行为或者需要一段注释才能让人理解的方法赋值 抽象 为函数,如果每个函数的 粒度 都很小,那么函数被 复用 的机会就更大,通过函数的组合来使聚合根更具有组合性,更复杂的方法,从而增加系统的灵活性与代码的复用(这也是重构一书中用得最多的重构手法)。而对方法进行自由的 组合 也是函数式编程的一大特性。同时,聚合根之间应通过聚合根进行交互,先用 manager 取得其他聚合根,然后再通过 FP 的方式进行操作:

    private final NoneParamConsumer deleteTaskByRequirementId =
            () -> taskManager
                    .getByRequirementId(this.data().getId())
                    .forEach(Task::deleteItSelfAndAssignee);

代码 4-1-1 示例中的 deleteTaskByRequirementId 函数,通过 taskManager 取得 Task 聚合根,再通过 FP 的方式执行逻辑

当函数式遇上响应式

虽说目前的的敏捷项目系统没有多少并发量,但是梦想还是要有的,毕竟我们是要做 Saas 平台的,说不定某天流量上来了。

终极目标是使系统具备良好的响应性,这样用户才会有比较良好的体验。这意味着系统必须在特定时间延迟之内响应用户的请求,这也被称为边界延迟。而且系统的延迟边界 同样还包含了失败,一个系统如果被失败卡住了,那显然它就不具备响应性。
要使模型具备响应性,需要确保构成它的所有模型也都具有响应性。一个模型所需要遵守的响应式架构的所有原则同样适用于构成系统的其他模型。整体系统的响应性 取决于组件中响应能力最差 的那个,也就是我们常说的木桶效应。

为什么是函数

函数使响应式更容易,对响应式建模来说,纯函数是最佳拍档。
如果函数不受副作用的影响,可以更有效率地使用并行数据结构,而不用考虑带来的并发问题。

先来看一下 敏捷项目管理系统中的一个缺陷接口:

在这里插入图片描述

在这里插入图片描述

下文将 Reactor、R2DBC 引入敏捷项目管理系统之中。比较一下引入响应式框架之后的代码:

在这里插入图片描述
在这里插入图片描述

显而易见的是 controller 和聚合根里的代码都没有什么变化,除了将原来的 Repository 替换为 ReactiveCrudRepository

public interface BugReactiveRepository extends ReactiveCrudRepository<BugDO,Long> {

}

BugReactiveRepository 通过继承 R2DBC 的 ReactiveCrudRepository,在进行 CURD 操作时将会返回 Mono 以及 Flux 这类单子。相信聪明的读者朋友们已经发现了,我们一样可以利用 Mono、Flux 这些单子,通过组合抽象将小用例组合成更大的用例。

_响应式流模型

参考

实现领域驱动设计(美)弗农著
函数响应式领域建模
vavr: https://docs.vavr.io/
project reactor: https://projectreactor.io/docs/core/release/reference/index.html
Spring Data R2DBC: https://docs.spring.io/spring-data/r2dbc/docs/1.1.0.RELEASE/reference/html/#get-started:first-steps:reactive

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值