Java微服务系列001领域驱动设计

31次阅读

共计 8855 个字符,预计需要花费 23 分钟才能阅读完成。

原文链接公众号:微小趋势 xiaoyongtalk

Part 1 DDD 基本概念

1.1 什么是领域驱动设计?

领域驱动设计(Domain Driven Design,简称 DDD),是一种指导软

件系统工程建设的方法论。

“通过抽象提取的手段,将复杂的领域业务细节抽象成精简的业务

模型及其实现的匹配。”

可以理解为:针对某个业务领域的,面向对象思维的回归和升华。

在面对一个新领域时,甄别哪些对象对系统有用?哪些对拟建系统

无用?如何保证选取的模型对象恰好够用——既不过度设计又能满

足业务要求?

1.1 什么是领域驱动设计?(续)

领域对象并不是独立存在的,对象之间有各种千丝万缕的联系(如

地铁站、线路和列车之间的关系),正是这种联系造成了系统的复

杂度。

很多时候修改一处变更,则牵一发而动全身,对象的封装机制仅仅

只能解决有限的部分问题。

通过 DDD,则可以保留对象之间有用的关系而去掉无用的关系,限定

变更影响范围来降低系统的复杂度。

1.1 什么是领域驱动设计?(续)

DDD 要求系统架构师、系统开发工程师等技术人员,与业务领域专家

达成业务上的共同认知。其基本方法则是站在业务角度,从顶层抽

象,用上帝视角来看待整体业务。

DDD 与传统 OO 的区别在于,前者是业务视角,后者是技术视角;前者

从整体出发建模后设计,后者是各种细节设计后堆叠出一个系统;

前者领域知识丰厚且可传递,后者将系统做成“四不像”……

DDD 要求技术人员将精力投入一部分到业务理解上,而不是单纯的做

技术工作。

1.1 什么是领域驱动设计?(续)

DDD 是一种较为抽象的设计指引,不会涉及具体某个业务领域的设计

实践,但国内有参考案例,如很多体量较大的互联网公司早就在实

践 DDD,特别是做业务中台的时候。

一般设计的时候需要考虑上下文环境,同时要跟业务领域专家深入

合作,如首先统一术语,明确需求边界,识别关键对象,理清关键

业务对象之间的关系等等。

DDD 在 Martin Flower 等人的传播下,目前已成为微服务架构必不可

少的指导思想。

1.2 DDD 并不是万能的

DDD 是 OO 的一种升华,但同样存在不足,如在安全、权限方面的考虑,

与开发框架如何融合等问题。

因此,DDD 与 OOP,MDD(模型驱动设计)和 MDA(模型驱动架构)等

理念并不是非此即彼的关系,而是要根据具体场景结合使用。

© MININGLAMP Technology 2019.

1.3 国内应用 DDD 的一线公司及领域

公司:thoughtworks,阿里巴巴集团,360 集团等

应用 DDD 的领域:金融、保险、电商等应用

未应用 DDD 的领域:区块链、大数据相关、AI 相关、纯技术中间件团

队……

先驱:Eric Evans(概念提出者)、Martin Flower( 推广者)……

© MININGLAMP Technology 2019.

1.4 构建领域知识 / 模型

领域模型:与业务领域专家交流,将零散的业务知识建立抽象——

在脑海里建立一个业务蓝图,随后不断完善使之越来越清晰。

领域模型不需具备一本百科普及类似的结构,而是经过严格组织并

选择性抽象后的知识——只选取对系统有用的领域知识,忽略掉无

关的部分。

领域模型贯穿设计和开发的全过程。如何取舍领域对象是设计阶段

的工作,如软件会关注客户的联系方式,但不太会关心客户眼睛的

颜色。

© MININGLAMP Technology 2019.

1.4 构建领域知识 / 模型(续)

模型是最基础部分,可以简化复杂问题的设计过程。

模型是用来与领域专家、架构师、开发工程师和测试工程师进行交

流的核心工具,因此需要做到精确、完整、无二义性。

模型的表现方式可以是图、用例、也可以是文档,甚至是伪代码。

模型也可以用来进行代码设计(非软件设计中所说的架构设计,这

里指具体的代码细节,如代码分层,如何分包等)

© MININGLAMP Technology 2019.

1.5 构建领域知识 / 模型——简单案例分解

地铁监控系统(假想需求):不考虑完整的地铁网路控制系统,只

跟踪有换乘线路的单个地铁站,判断某一条线是否遵照了预定的线

路,以及是否有可能发生碰撞。

领域专家:地铁航线管理或监控专家,但不能指望他们完整的描述

这个领域的所有问题。他们可能会描述的内容包括:列车线路号、

进站时间、出站时间、上一站、下一站等知识。而我们需要从这些

看似杂乱的信息中找到规律。

© MININGLAMP Technology 2019.

1.5 构建领域知识 / 模型——简单案例分解(续)

由此就抽象了三个基本对象:列车,起始站和目的站(可以合并为

同一个对象用状态区分)。然后他们之间的关系又会引发出一个新

的对象:路线。

于是我们进一步整理(交流 -> 抽象 -> 反馈 - 进一步交流):

© MININGLAMP Technology 2019.

1.5 构建领域知识 / 模型——简单案例分解(续)

到这一步,是通过列车和上一站与下一站总结出来的结论,但实际

上一列列车从起点站到终点站之间,会组成一条完整的曲线,甚至

还要区分内圈与外圈等多条曲线。

简单来说,这些曲线是由很多上一站与下一站组成的。

© MININGLAMP Technology 2019.

1.5 构建领域知识 / 模型——简单案例分解(续)

列车的形式并不是由司机决定的,假设都会有列车的出行计划。

则我们可以进一步将模型整理成下面这样(现实肯定复杂很多倍,

这里仅是一个例子)

© MININGLAMP Technology 2019.

1.6 通用语言

业务人员满嘴领域专业词汇(行话),开发人员满脑子类、对象、

设计模式、继承、多态、抽象等,甚至两类人的思维模型天差地别,

交流起来就类似“鸡同鸭讲”, 所谓隔行如隔山。

DDD 核心原则之一:使用基于模型的语言。确保团队使用的语言在所

有的交流形式中保持一致,这种语言成为“通用语言(Ubiquitous

Language)”

通用语言成型方式:介于领域术语和开发术语之间的,通过类比和

举例两种方式达成理解上的一致后,形成名词集合。(个人实操经

验,仅供参考)

© MININGLAMP Technology 2019.

1.6 通用语言

当一个领域的通用语言成熟以后,是可以直接由开发人员转化为代

码中对象的表示的,甚至单词都可以不用变化。

如此一来,代码的可读性得到提高,模型也完美地呈现了。当模型

逐步变大时,技术上的意外可以得到一定程度的控制。

© MININGLAMP Technology 2019.

1.7 小结

本部分结合一个简单的例子,介绍了 DDD 相关的基本概念和部分方法。

这属于理论层面的内容,更多的属于在项目前期需求交流和设计阶

段的工作。

接下来的部分将介绍如何依赖领域模型进行落地的方法:将领域模

型设计的内容转换为具体的代码。

© MININGLAMP Technology 2019.

Part 2 模型驱动设计

© MININGLAMP Technology 2019.

2.1 模型驱动设计——概述

理论总是完美的,落地是有困难的。技术人员如何经受生产环境的

考验?能做到易扩展和易维护吗?

领域可以从不同角度被表现为多种模型,但只能选择容易被轻易和

准确转换为代码的模型。

推荐的做法是在模型设计阶段就让开发人员参与讨论,在建模的过

程中,同时考虑如何转换为代码的问题,形成良性的反馈机制——

问题越早被识别出来,就越容易被解决。

© MININGLAMP Technology 2019.

2.1 模型驱动设计的基本构成要素

© MININGLAMP Technology 2019.

2.2 模型驱动设计——分层架构

© MININGLAMP Technology 2019.

2.2 模型驱动设计——分层架构(续)

不分层的代码无论是阅读还是维护都及其困难。

分层的原则与高内聚低耦合的理念不谋而合。

一般分层分为 UI、应用、领域和基础设施,其中基础设施一般与业

务无关,属于技术组件级别(中间件、开发框架等)。

在这其中,领域部分是本文重点研究的对象。

© MININGLAMP Technology 2019.

2.2 模型驱动设计——分层架构(续)

层释义用户界面 / 展现层负责向用户展现信息以及解释用户命令 / 自演示指引 / 操

作手册……应用层很薄的一层,用来协调应用的活动。

不包含业务逻辑。

不保留业务对象的状态,但保有应用任务的进度状态。领域层包含关于领域的信息。

是业务软件的核心所在。

这里保留业务对象的状态,对业务对象和他们状态的持

久化被委托给了基础设施层。基础设施层作为其它层的支撑库存在。

提供了层间的通信,实现对业务对象的持久化,包含对

用户界面层的支撑库等左右。

包括 DB、缓存、MQ、开发框架等基础组件……

© MININGLAMP Technology 2019.

2.3 模型驱动设计——实体

实体是有标识符的一种对象,在 java 中一般表现为 POJO。标识符一

般可以描述为 ID,如用户编号、银行卡编号等,对应到数据库中,

一般表示业务主键,或者是有唯一索引的字段。

不到万不得已,尽量不使用属性去映射对象(常见的通过姓名和身

份证号去映射客户)。

© MININGLAMP Technology 2019.

2.3 模型驱动设计——实体(续)

1. 失血模型:模型仅仅包含数据的定义和 getter/setter 方法,业务

逻辑和应用逻辑都放到服务层中。这种类在 Java 中叫 POJO, 在.NET

中叫 POCO。

2. 贫血模型:贫血模型中包含了一些业务逻辑,但不包含依赖持久

层的业务逻辑。这部分依赖于持久层的业务逻辑将会放到服务层

中。可以看出,贫血模型中的领域对象是不依赖于持久层的。

3. 充血模型:充血模型中包含了所有的业务逻辑,包括依赖于持久

层的业务逻辑。所以,使用充血模型的领域层是依赖于持久层,

简单表示就是 UI 层 -> 服务层 -> 领域层 <-> 持久层。

4. 胀血模型:胀血模型就是把和业务逻辑不想关的其他应用逻辑

(如授权、事务等)都放到领域模型中。我感觉胀血模型反而是

另外一种的失血模型,因为服务层消失了,领域层干了服务层的

事,到头来还是什么都没变。

© MININGLAMP Technology 2019.

2.3 模型驱动设计——值对象

实体是可以被跟踪的,但跟踪和创建标识符需要很大的成本,特别

是涉及 DB 分片的系统。也可能带来性能问题,因为需要对每个对象

产生一个实例。

当只关心一个对象所拥有的属性而不需要其标识符的时候,可以使

用另一类对象:值对象(Value Object, 简称 VO)。也有人称之为数

据传输对象(Data Transfer Object,简称 DTO)。

值对象相对实体对象而言,是可以轻易创建和丢弃的,一般设计为

不可变类。同时 VO 应该尽量保持简单。

© MININGLAMP Technology 2019.

2.3 模型驱动设计——值对象(续)

值对象可以包含其他的值对象,甚至可以包含对实体对象的引用。

值对象的命名一般是在实体对象名称之后加上 VO 或 DTO 字样以示区别,

也可以单独分包,但仅通过包名来区分的话,容易引起误会。

VO 一般通过构造方法进行初始化,也可以通过 builder 的方式进行。

© MININGLAMP Technology 2019.

2.4 模型驱动设计——服务

实体对象和值对象应该尽量减少行为,仅仅包含属性和对属性进行

set 或 get 的方法。

比如设计一个账户,当涉及转账操作时,无论是将这个动作放在转

出账号上还是转入账号上,都显得很别扭。

类似这样用来形容动作或者行为的,应该单独声明为一个服务。服

务用来协调,服务于实体和值对象的相关功能进行分组。

服务担当了一个提供操作的接口。

© MININGLAMP Technology 2019.

2.4 模型驱动设计——服务三个特征

1、服务执行的操作涉及一个领域概念,这个领域概念通常不属于一

个实体或者值对象;

2、被执行的操作涉及到领域中的其他对象;

3、服务的操作应该是无状态的,仅仅表示一个动作或行为。如转账、

客户注册、短信发送等……

© MININGLAMP Technology 2019.

2.5 模型驱动设计——模块

当模型越来越大,就需要模块来组织相关概念和任务,以降低复杂

度。

另一个原因提升代码质量。当涉及到功能性内聚和通信性内聚要求

时,使用模块。

模块应该由在功能上或逻辑上属于一体的元素构成,同时具有定义

好的接口,提供给其他模块访问。

提供的接口应该是封装完整以后,以减少对方调用接口的数量为主

要目的。

© MININGLAMP Technology 2019.

2.6 模型驱动设计——聚合、工厂和资源库

聚合:用来定义对象所有权和边界的领域模式。

工厂和资源库:处理对象的创建和存储问题。

© MININGLAMP Technology 2019.

2.6 模型驱动设计——聚合

一个模型会包含众多领域对象,领域对象之间会形成一对一、一对

多或对多对关联,从而形成了一个复杂的关系网。

1、删除非本质的关联关系:在领域中存在,但在模型中不必要。

2、通过增加约束的方式消减多重性。

3、将双向关联转换成非双向的关联,如汽车拥有引擎,但引擎并不

需要拥有汽车。

© MININGLAMP Technology 2019.

2.6 模型驱动设计——聚合(续)

聚合使用边界将内部和外部的对象划分开,每个聚合有一个根,也

就是一个实体对象,并且它是外部可以访问的唯一对象。

外部对其他对象的引用,全部由这个跟对象来进行访问。(类似于

企业组织架构之间沟通的意思,总经理只找总监,总监再找下面的

人。)

© MININGLAMP Technology 2019.

2.6 模型驱动设计——聚合(续)

© MININGLAMP Technology 2019.

2.6 模型驱动设计——工厂

当实体的属性很多时,通过构造器来创建对象是非常困难的,代码

可读性也非常差,也与领域本身所做的事情相冲突。在领域中,某

些事物通常是由其他事物组合的(如汽车装配~~)。

这里的工厂概念与设计模式的类似,可以理解成简单工厂模式、抽

象工厂模式和工厂方法模式的并称,因需选用对应的模式即可。

© MININGLAMP Technology 2019.

2.6 模型驱动设计——工厂(续)

© MININGLAMP Technology 2019.

2.6 模型驱动设计——资源库

资源库:封装所有获取对象引用所需的逻辑。简单来说就是对获取

数据的操作进行封装。

© MININGLAMP Technology 2019.

Part 3 重构与一致性

© MININGLAMP Technology 2019.

3.1 持续重构

字面含义

© MININGLAMP Technology 2019.

3.2 凸现关键概念

让隐式的概念显式化。

挖掘模型概念的方式有:使用领域文献;与领域专家深入交谈等。

从代码的例子,如 HashMap 扩容的约束,判断是否需要扩容时,将判

断条件(约束)提取为单独的 boolean 方法,就是一种手段。

凸现关键概念,在领域建模过程中和设计过程中会交互产生,但并

不会表现得很明显,很多时候是在潜移默化的过程中发生的。

© MININGLAMP Technology 2019.

3.3 保持模型一致性

接下来的章节全部是针对跨团队合作大型项目群中出现的问题所提

供的解决思路。

多个团队之间需要合作,同时又是并发开发。比如项目组 A 定义了一

个模型,项目组 B 在开发时觉得少了一部分东西,于是就增加或修改

了部分内容,但项目组 B 的这位开发人员没有意识到,这个动作实际

上是对模型进行了变更。

如此相互修改后,就会导致系统实现上完全背离了当初的设计,从

而导致系统出现问题。

想想自己平时校验数据的方法?用 isNull 还是 isNotNull?

© MININGLAMP Technology 2019.

3.3 保持模型一致性(续)

© MININGLAMP Technology 2019.

3.3 保持模型一致性——界定的上下文

每一个模型定义一个上下文,一个独立的模型,上下文是固定的。

定义不同模型间的边界和关系,不能在不同模型间传递任何对象,

也不能在没有边界的情况下激活行为。

不同模型的代码不能合并。模型应该足够小,直到只有逻辑相关以

及能形成自然概念的因素放在一个模型中为止,同时也要一个小团

队能够实现(转换为代码)。

万不得已不同的团队要在同一个模型上工作时,要时刻注意不要踩

到别人的脚(各司其职,不越边界)。

© MININGLAMP Technology 2019.

3.4 保持模型一致性——持续集成

当界定的上下文定义以后,就需要保持模型的完整性和一致性,不

能再继续分割模型。

一开始定义模型是不完美的,一般是先创建模型,然后反复持续完

善。

使用持续集成,确保新增的部分和模型原有的部分配合得很好,在

代码中也能被正确地实现。

对 3—7 人的独立小团队而言,每日合并代码是比较推荐的做法。

合并的代码需要自动构建并执行自动测试(单元测试、Mock 等)。

© MININGLAMP Technology 2019.

3.5 保持模型一致性——上下文映射(Context Map)

每个团队在自己的模型下工作,但最好能了解所有的模型。

上下文映射是指抽象出不同界定上下文和它们之间关系的文档,可

以是一个图,也可以是其他文档。

© MININGLAMP Technology 2019.

3.6 保持模型一致性——共享内核

在上下文之间,共享内核(Shared Kernel)和客户 - 供应商

(Customer-Supplier)是具有高级交互的模式。

隔离通道(Separate Way)是在我们想让上下文高度独立和分开运

行时要用到的模式。

还有两个模式处理系统和继承系统或者外部系统之间的交互,它们

是开放主机服务(Open Host Service)和防崩溃层

(Anticorruption Layer)。

© MININGLAMP Technology 2019.

3.6 保持模型一致性——共享内核 (续)

共享内核的目的是减少重复,但是仍保持两个独立的上下文。对于

共享内核的开发需要多加小心。

如果团队用的是内核代码的副本,那么要尽可能早地融合(Merge)

代码,至少每周一次。还应该使用测试工具,这样每一个针对内核

的修改都能快速地被测试。

内核的任何改变都应该通知另一个团队,团队之间密切沟通,使大

家都能了解最新的功能。

© MININGLAMP Technology 2019.

3.6 保持模型一致性——共享内核 (续)

共享内核的目的是减少重复,但是仍保持两个独立的上下文。对于

共享内核的开发需要多加小心。

如果团队用的是内核代码的副本,那么要尽可能早地融合(Merge)

代码,至少每周一次。还应该使用测试工具,这样每一个针对内核

的修改都能快速地被测试。

内核的任何改变都应该通知另一个团队,团队之间密切沟通,使大

家都能了解最新的功能。

© MININGLAMP Technology 2019.

3.7 保持模型一致性——客户与供应商

经常会遇到两个子系统之间关系特殊的时候:一个严重依赖另

一个。

两个子系统所在的上下文是不同的,而且一个系统的处理结

果被输入到另外一个。而且没有共享内核。

在两个团队之间确定一个明显的客户 / 供应商关系。在计划场景里,

让客户团队扮演和供应商团队打交道的客户角色。

为客户需求做充分的解释和任务规划,让每个人理解相关的约定和

日程表。

联合开发可以验证期望(Expected)接口的自动化验收测试。

© MININGLAMP Technology 2019.

3.8 保持模型一致性——顺从者

供应商团队并不一定会做得很好。利他精神、自己的 deadline 等会

导致为客户团队服务的力度不够,客户团队大多数情况下比较无助。

如果客户不得不使用供应商团队的模型,而且这个模型做得很好,

那么就需要顺从了,而且要完全顺从。

这个模式与共享内核相似,但最大的区别是,客户团队没有任何权

限修改模型。

示例:一般称为“核心”系统会作为供应商,“前置”“应用”等

为客户。常用做法是提供一个大而全的模型,甚至还会提供一个

JSON 串用来防止临时需要的发生。

© MININGLAMP Technology 2019.

3.9 保持模型一致性——防崩溃层

不同团队之间会有模型交互,如接口。

不能忽视和外部模型的交互,但是我们也应该小心地将我们的模型

和它隔离开来。方法就是在自己的客户端模型和外部模型之间,建

立一个防崩溃层。

实现:一个非常好的方案是将这个层看作从客户端模型来的一个服

务。

一般来说是 Facade 和 Adapter 的组合。

© MININGLAMP Technology 2019.

3.9 保持模型一致性——防崩溃层(续)

© MININGLAMP Technology 2019.

3.10 保持模型一致性——独立方法

独立方法模式适合一个企业应用可由几个较小的应用组成,而且从

建模的角度来看彼此之间有很少或者没有相同之处的情况。

创建独立的界定上下文(Bounded Context),并独立建模。

这样做的好处是有选择实现技术的自由。如有些团队使用 Java、有

些团队使用 Node.js、还有些团队使用 Go 或者 Ruby 等。

然后再通过一个瘦的 GUI 或者类似门户的形式,将这样的小应用组合

起来,通过按钮点击不同系统结合单点登录的方式来使用。

© MININGLAMP Technology 2019.

3.11 保持模型一致性——开放主机服务

集成两个子系统时,通常要在它们之间创建一个转换层,用来扮演

缓冲的角色。但如果需要集成的系统较多,就是一个灾难。系统难

以维护,调整极度困难。

定义一个能以服务的形式访问子系统的协议。开放它,使得所有

需要和你集成的人都能获取到。然后优化和扩展这个协议,使其可

以处理新的集成需求,但某团队有特殊需求时除外。

特殊的需求在协议之上再建立一个转换层。

这种做法,叫开放主机服务。

© MININGLAMP Technology 2019.

3.12 保持模型一致性——精炼

一个大的领域会有一个大的模型。即使在重构多次之后,也依然会

很大。

对于这样的情况,就需要精炼。

思路是定义一个代表领域本质的核心域(Core Domain)。精炼过程

的副产品将是组合领域中其他部分的普通子域(Generic

Subdomain)。

在之前的例子中,将进站站点和出站站点统一为经纬度坐标的过程,

其实就是一次精炼的过程。

本文版权归作者所有,欢迎转载,转载必须在醒目位置标明作者与出处.

正文完
 0