从软件复杂度的角度去理解DDD

简介: 从我们作为业务开发主要的职责深入到DDD的本质是什么?复杂度应处理?规范设计怎么做?本文将全方位为大家解答。

一 作为业务开发,我们的主要的职责是什么的

业务开发的职责

在文章的开始我想和大家一起思考一个问题: 作为一个工程开发,我们最主要的职责是什么?

我极度认可 <<浅谈什么是技术一号位>>【1】文章的观点 - 切实解决业务问题才是每一个工程开发最主要的职责 - 所以每个业务开发都必须要结合业务的视角去思考自己系统的建设和发展,而不是只是做一个“编程的”码农。

这里摘录一下文章中要点

  • 技术一号位是负责使用技术能力解决业务问题,提供稳定可靠的技术支撑;
  • 负责向业务各方提供各种必要的技术支撑,通过合理的数据分析为业务决策提供依据;
  • 通过对技术领域的积累和发展,通过业务领域的理解和落地影响业务决策;
  • 负责构建梯队完整、能力全面、制度完善的技术团队来支撑业务发展。

文中也提到了虽然不是每个人都负责一块完成的业务,也不是每个人都带领团队,但是至少每个人都是自己所负责的那块系统的技术一号位。



业务在实际开展中遇到的问题

那实际业务开展中,业务到底会遇到有哪些问题呢? 我们按业务的生命周期进行切分,然后具体查看每个业务生命周期的诉求:

  • 业务启动期:业务能力快速搭建 - 系统提供快速试错的能力
  • 业务发展期:业务能力扩展  -  系统需要支持原来越多的业务功能
  • 业务平台期:业务能力复制 - 系统需要支持原来越多的业务场景
  • 业务衰退期:业务能力创新 - 系统提高生产力延长业务的生命周期

image.png

我们技术要做的事情是:在业务验证没有问题的情况下,如果尽可能的延长业务的发展和平台期,让业务获取的利益最大化。所以为了支持业务的发展,业务的本身的功能支持诉求以及业务对技术的要求也会越来多,在这种情况下考验软件开发人员的一个非常关键的能力就是: 软件复杂度的控制的能力


软件复杂度

软件复杂度其实是一种多维度的概念,其可能来源于多个方面,前阿里资深技术专家李运华在他的《从0开始学架构的》课程中从6个方面阐述了软件复杂度【2】,列举如下:

  • 高性能
  • 单机性能
  • 集群性能
  • 高可用
  • 计算高可用
  • 存储高可用
  • 可扩展性
  • 低成本
  • 安全
  • 规模
  • 业务规模
  • 系统物理规模



二 DDD的本质是什么

DDD本质上我认为就是一种减低软件复杂度的手段, 其推荐的方法论可以适用于上面包括了业务规模可扩展性两个维度的复杂度应对。 其实业务规模的复杂度的处理包括了对可扩展性的支持。

DDD实施给系统之后,我们依然需要关注系统其它的复杂度,这里列举一些示例措施:

  • 容量规划
  • 架构设计
  • 数据库设计
  • 缓存设计
  • 框架选型
  • 发布方案
  • 数据迁移、同步方案
  • 分库分表方案
  • 回滚方案
  • 高并发解决方案
  • 一致性选型
  • 性能压测方案
  • 监控报警方案

那么我们进一步对业务规模的复杂度进行拆解,又分为下面两类:

1 领域复杂度

  • 领域模型描述问题域的准确性

2 技术实现的复杂性

代码没有按照业务绑定的”分析模型”去编码,软件变成一个大泥潭

  • 软件的可扩展性较差
  • 软件变成面向过程
  • 分层不合理
  • 没有规范

那DDD是如何处理上面提到的软件复杂度的?

  • 提供了一个领域划分的方法:让软件系统产生边界。
  • 提供一个一系列的战略模式:限界上下文的映射,分层架构等。
  • 提供一个一系列的战术模式:如何规划领域层 内部

DDD不是什么?

  • 不光光只是一种编程方法
  • 不光光只是一种架构风格
  • 不具体指导如何具体建模



三 复杂度处理-领域模型描述问题域的准确性

DDD的原名是模型驱动的设计方法:通过领域模型(Domain Model)捕捉领域知识,使用领域模型构造更易维护的软件。


合理性证明

DDD的核心思想,大家都清楚,就是分析模型要和代码模型保持一致。

那么如果不保持一致到底会产生什么样的负面影响

image.png


如果技术实现和业务实现不在用一水平线上,那技术模型的行进路线只会考虑劈开技术障碍并且可能会撞在未来的业务障碍的墙上。 这样就很容易出现,业务持续演进等技术想实现的时候,却发现当前的实现依赖于“业务不会这样发展”的假设上。 这也是为什么会出现现在众多业务需求,技术无法实现或者是需要花大量时间去实现的原因。


但是如果技术和业务通过统一语言打破知识的壁垒保持一致,那么如果后面技术遇到问题即是业务碰到的问题,业务人员需求的变更和迭代会自然而然的帮助技术同学越过一些门槛。  也就是说业务方与技术方参与到对方的工作中,就在双方之间带来了更好的协同,形成1+1>2的功效


什么是问题域

根据百度百科的解释【3】 在软件工程中,问题域是指待开发系统的应用领域,即在客观世界中由该系统处理的业务范围

image.png


那么问题域内的组成是什么呢? 就是我们的域模型。

这里直接摘抄一段前阿里P10"阿白"在阿里内部发表的域模型的观点:

域模型(domain model)英文又称为问题域模型(problem space model)。维基百科(Wikipedia)对它的定义是” A conceptual model of all the topics related to a specific problem” 可以翻译成: “域模型是针对某个特定问题的所有相关方面的抽象模型”。 这个定义有几个要点:第一是“特定问题”, 也即是说域模型是针对性某个问题域而言的, 脱离的这个特定问题,域模型的构建其实不存在一个最优或者是最合理的构建。 第二是抽象, 域模型是一个抽象模型, 不是对某个问题的各个相关方面的一个映射, 也不是解决方案的构建。

image.png


如何实现问题域的分析

在 DDD 中,Eric Evans 提倡出一种叫做知识消化(Knowledge Crunching)的方法帮助我们去提炼领域模型。简单来说就是五个步骤:

  • 关联模型与软件实现;
  • 基于模型提取统一语言;
  • 开发富含知识的模型;
  • 精炼模型;
  • 头脑风暴与试验。

image.png


开发人员和业务专家在一起通过一个个业务用例仔细讨论应用程序的应用场景,从而使得业务人员深刻理解业务知识,开发人员和业务人员就重要的业务概念建立起统一的语言,开发人员将这些概念根据业务用例的上下文抽象出模型,并且这些模型将会最终成为最终软件实现中的领域模型。 随后随着更多的业务用例的输入,开发人员和业务人员会逐渐对已经构建的模型进行精化,并且也会用新的用例去检验之前构建模型的合法性和适用性。


DDD在这一步其实没有给出详实标准的如何建模的方法,毕竟建模还是来自于每个人的世界观,其过程还是倾向于经验的。但是还是有不少人总结一些标准的建模方法论例如:

1 四色原型法  http://apframework.com/2020/03/22/ddd-color/

2 用例分析法 https://baike.baidu.com/item/%E7%94%A8%E4%BE%8B%E5%88%86%E6%9E%90/2859078?fr=aladdin



问题域的拆分

大家应该发现上面的知识消化的流程是一个非常耗时和复杂耗脑力的过程, 涉及到产品,业务,技术等多方团队, 所以为了让有限的资源投入到最最核心的子域,我们需要对问题域进行这份,把重点的精力放到最核心的领域上。


核心领域一定是业务价值最高的,而非技术难度最高或者是基础设施框架部分。


要切分问题域,首先需要了解问题域的种类:

1 通用域:  非应用独有的,多个应用都会有的功能。 例如发送邮件,触达等

2 核心域:和竞争对手区别开来的区域,或者是在市场上被赋予了竞争优势的区域。

3 支撑子域:其余的区域


如何确定核心域,这里有几个提示:

  • 系统哪部分最难用
  • 手动处理过程阻止了他们进行了根据创造性, 有附加值的工作
  • 哪些修改能提高收益
  • 哪些修改能提高运营效率

取哪些提示,取决于业务系统的性质。



那如何决定支撑子域/通用子域,以及支撑子域/通用子域的切分呢?

目前在我查阅的资料中,还暂时没有人提及到具体的操作方法,感觉主要还是依靠经验主义在做划分。 我个人总结了一个方法,主要就是就是关注业务的核心实体和核心流程。 以核心实体和核心流程作为切分支撑子域的基础。

核心实体:核心实体是存在于核心流程中,对核心流程的决策和扭转可能起到关键的作用。有的时候业务上为了能让核心实体在业务流程中起到更大或者更高效的作用,会添加一些让核心实体更好服务于业务流程一些业务功能,从而使业务实体从整体上看变得相对复杂,这个时候我们应该以核心实体为基础进行切割,把所有和核心实体CRUD相关的操作还有让其变得更高效的业务功能划分为单独的一个领域。

核心流程: 当某个业务流程足够复杂也可以当成一个子域。


在实现领域驱动设计【4】书中,提到了为在线拍卖网站系统划分问题域的一个例子,我们以此来验证上面等构想

image.png

划分子域

image.png

  • 卖家 + 会员身份: 这两者都是核心实体,网站可能为了让促进会员能够多参与拍卖可能提供了分层,或者积分等工功能。 网站为了能让卖家能够更加提供更加有拍卖价值或者是转化率高的品类可能为卖家提供了数据分析等业务功能。
  • 名册:这也就是核心实体,网站会对名册提供一系列拍卖相关的功能,例如倒计时,一口价等,所以也需要形成一个领域。
  • 拍卖:网站最核心的业务流程,核心域无疑。
  • 争议解决:买卖家的售后冲突解决流程向来很复杂,所以会独立成为一个域无疑。



四 复杂度处理-进一步降低问题域的复杂度-限界上下文

限界上下文的诞生背景

一般情况下,一个复杂系统由一系列的模型来表示解答域, 理想状态是一个子域一个模型。 但是有些当业务需要且系统复杂的时候,一个模型可能被多个域共享,这个时候这个模型的概念可能变得不清楚。因此为了保护这些模型概念的完整性, 清晰的定义模型的责任边界很重要。


实现领域驱动设计【4】书中举了下面这个例子:


image.png


为了维护模型的概念的完整性,最直观的方法就是为这个模型化一个边界,e.g. 这个商品所表现的意思就是履约的时候用到的"商品",而不是下单的时候的"商品"。 只要有一个这样的边界定义,系统就会但是出现多个边界,毕竟"商品"在不同业务上下文中有不同的含义, 例如库存域的货品,物流域的运输品, 价格域的商品等等。这样的一个边界就是DDD的“限界上下文”。

限界上下文给人直观的感受其实和子域很像,我很早以前曾读过一些关于微服务的书籍,也提到过要把DDD中的限界上下文作为微服务划分的重要依据。 这里其实就给我很大的疑惑:

1 限界上下文到底是怎么划分的? 我们划分限界上下文难道真的是用一个基础概念,然后找这个基础概念不同的“上下文”吗?

2 限界上下文和子域到底区别是啥?



限界上下文的本质

DDD理论中提到了DDD的四个边界

image.png

所以在DDD中是把限界上下文作为某个子域的内部模块的划分,其实无论是子域的划分,限界上下文的识别,和聚合的划分他们的本质是一样的,他们都是对复杂问题的分解之后,然后归类分组。只不过“聚合”面向的是领域层内部,“领域”划分面向的是业务问题域,而“限界上下文”面向的是解答域,但是我跟倾向于把限界上下文理解为更加深一层次的业务问题域的划分,而不是面向的解答域。


如果这样看的话,那么其实就可以回答上面的疑问, 领域和限界上下文没有本质的区别,就像树的父节点和字节点一样都是树节点。 而限界上下文的划分完全可以使用子域划分的理论。(可以回顾下上面问题域拆分的段落)



上下文映射

上下文的映射是什么, 简单来说就是描述不同上下文之间的关系的描述。举个例子

image.png

DDD对于限界上下文直接提炼了几种方式,这里这边阿里内部文章《领域驱动设计:软件复杂性应对之道》解释的比较好,描述如下:

shared kernel

共享内核 shared kernel :通常是共享核心领域或者是一组通用子领域。

image.png

customer/supplier

客户/供应商关系 customer/supplier:上下游关系。不同客户需要协商来平衡,上游团队需要有自动测试套件。

image.png

conformist

跟随者模式 conformist:单方面跟随模式。上游的设计质量较好,容易兼容,可以采用严格遵循上游团队的模型。

image.png

anticorruption layer

防腐层 anticorruption layer:防腐层、隔离层,使用 facade or adapter 等模式。可以减少其它系统变动对本系统的影响。

image.png

separate way

各行其道 separate way:声明一个与其它上下文毫无关联的 bounded context,使开发人员能够在这个小范围内找到简单、专用的解决方案。

image.png

open host service

开放主机服务 open host service:开放子系统供其他系统访问。其核心思想是开放出一个标准的各个领域都认可的协议,减轻各个领域实施ACL的负担和成本。

image.png

published language

共享语言 published language:把一个良好文档化、能够表达领域信息的共享语言作为公共的通信媒介,必要时在其它信息与该语言之间进行转换。



在当前电商领域的范畴,目前我个人觉得只有ACL,Seperate Way, publish language 有比较好可行性,其他的关系都不是很靠谱:

  • shared kernel: 如果使用共享二方库,谁来维护这个二方库,如何防止在不同上下文使用不同kernal版本所带来的问题。

如果一定能保证shared kernel的维护在一个团队内,且所有使用shared kernel版本一定能保持一致, 那是可以使用的。

  • customer/supplier: 我曾经因为汇率包升级而去重构一个应用,因为汇率包变更太大,且应用没有防腐层,所以不论从开发还是测试都是非常痛苦的过程。
  • conformist: 和customer/supplier类似, 但是在互联网领域没有靠谱的设计, 只有有人维护和没有人维护的设计。 conformist从长期来看其实就是customer/supplier。
  • Open Host Service 没有任何一个领域保证自己的接口一定不会变,就算不会,其他领域的同学会相信吗,他们会忍住不用ACL吗? 如果他们用ACL,OHS的意义何在?
  • publish language 目前阿里内部MTOP,TOP等协议正是使用这样的协议。

另外限界上下文之间真的能够随便无规则无条件的互相依赖,互相调用吗? 在下面的章节将会解释论述。


五 复杂度处理 - 分层不合理

架构分层主要的作用就是关注点隔离,如果和今天的话题联系起来就是领域模型和技术的关注点隔离(领域和存储,领域和展示)。

传统的三层架构

image.png

这种传统架构的缺点

1 业务逻辑层和数据访问层有明显的耦合。

2 没有领域的概念,所有的逻辑沉淀到service中。

所以传统架构只能针对小型的,没有过多的业务逻辑场景。由于这种架构能够保有领域能力的沉淀,所以在现在电商业务场景基本不会被使用。


六边形架构

Alistair Cockburn在 2005 年时演示了 六边形架构

image.png

image.png

从六边形架构开始,其强调了领域模型。 并且确立了领域模型的核心位置,以及其不应该依赖于其他的层次。 六边形架构也强化了适配器的概念,其还把适配器分类为input适配器,和output适配器。 所有input适配器用于对接不用的外部请求形式, 所有output适配器用于对外部的依赖 (e.g. 数据库, 外部服务,内存调用等)


这种结构模式树立了以领域模型为核心的先河,但是其忽略了在领域层中跨模型业务逻辑的实现方式-领域服务的沉淀。 这也间接导致了其需要强化应用层,并且通过应用层和output适配器的联合去完成一些可以应该在领域层应该完成的事情。


洋葱架构

image.png

洋葱架构的提出更加进化了一步,推出了域服务层,并且支持域服务层是支持了那些需要多个领域实体联合中作用的领域逻辑. 其层次由外向内依次是领域模型,领域服务,应用服务和外层的基础设施和用户终端。其依赖的关系也只能是由外向内.  在洋葱结构中其把存储层,文件系统和网络服务放到了基础设施层。 由于基础设施和用户终端一样在最外层,所以洋葱架构也提倡用依赖倒置来解决应用逻辑和基础设施的耦合问题。

洋葱架构的架构图从其依赖顺序上来看,其依赖应用层必须先依赖域服务层,再依赖域模型层, 这样很容易造成领域模型的逻辑外泄到领域服务层,造成领域模型变成贫血模型。

DDD 架构

image.png

DDD的架构大家都非常熟悉了,领域服务和领域模型都归属域领域层。 适配层依赖域应用层,应用层依赖域领域层,也可以直接直接调用基础设施层(大多数是查询场景)。 领域层理论上不依赖于任何层次,其通过依赖倒置和基础设施层产生关联。在DDD架构中,应用层是可以通过直接访问聚合根(某个实体类),并进行方法的执行和操作的。应用层也可以直接访问基础设施层。 可以看出DDD的架构其实更加的贴切实际一些。

image.png

上面这张图也很好的阐述了DDD各个架构层次依赖的关系。



CQRS

很多情况产品构建出来的数据展示,需要横跨几个领域的数据的支撑,也就是我们日常构建的大宽表,在这种情况使用CQRS模式可以完美解决这个问题。其主导视图模型和领域模型分开,让领域模型更加专注业务逻辑,流程和规则而非业务视图。

image.png

CQRS的思想很简单,就是把服务中对数据的更新操作(Command)和读取操作(Query)分离, 一部分逻辑只处理和数据更新有关的业务,另外一部分只处理和数据读取有关的逻辑。这种处理方式,可以让我们辛苦构建的领域模型不被业务中所需要的这类视图需求所干扰。

CQRS 的两种实现方式

基于event- sourcing

image.png

不基于event - sourcing

image.png

以上的图片摘自于阿里内部的文章《CQRS模式及其应用



我们团队里面的架构实践

在我们自己的应用中我们构建了基于COLA【5】规范的层次架构,如下图:

image.png

我们也对自己的架构定义的一些额外的规范:

1 依赖关系(除了依赖倒置)只能是从上当下

2 同层之间永远不能互相依赖

3 如果同层之间需要互相用到对方的服务,那么就需要下沉出一层。例如在上图中,我们的业务层就分为了两层 "Executor"层次和 “Handler”层此, Handler层次用来保存业务的一些通用逻辑。



六 复杂度 - 软件变成一个大泥潭

从这一章节开始介绍DDD的"战术模式",也就是向大家介绍DDD是如何构建和组织自己领域层的。 值得一提的是在DDD中,领域的划分, 领域层次的建立, 领域之间关系的建立我们一般叫做DDD的"战略模式",而此章节提到的值对象,实体,域服务,工厂,repository, 聚合/聚合根, 领域事件等都是DDD的战术模式。战略模式的重要性是要远大于DDD的战术模式的,我们如果在领域划分,领域通信协议,分层方面没有大的问题, 那么即使再糟糕系统整体也还是可控的。


在领域层面, DDD通过聚合/聚合根的概念来划分单个领域中的类似于类集合的边界,从而降低单个领域层的复杂度。 DDD通过实体,值对象,领域服务,repository, factory 来规划集合内部的类组织, 另外DDD也通过领域事件来处理领域之间的交互,来匹配异步和需要解耦的业务场景。



实体

当我们需要考虑一个对象的个性特征,获取需要区分对象的时候,就需要引入实体。一般我们发现实体概念,是在和业务产品人员或者领域专家讨论发现的那些需要有唯一标示性或者生命周期连续性很重要的时候。


举个例子加入用户需要预定酒店,如果领域专家说了我们定了A酒店了,就不能定B酒店了,哪怕A,B其他的属性完全一样。 从领域专家扣中我们可以识别出酒店是有唯一标示性的,且哪怕A,B属性一样,也不能认为A,B 是一样的,这也说明了酒店的唯一性不是从属性来的。 这两点我们可以推断酒店是一个实体。唯一标示性可以是现实有意义的,例如工商注册号,也可以无现实意义,例如数据库主键。


实体建模的注意点

1 为实体分配唯一标识符

  • 现实意义标识符
  • 人工生成的标识
  • 自增
  • guid/uuid/
  • 数据库主键
  • 自定义sequence

2 验证和不变行

实体必须自己负责自己保持自己状态的合法性 (validation) 和不变性(Invariants)。他们的区别是合法性是根据上下文的,而不变性是不用考虑上下文且必须正确的。 例如酒店必须有房间这个就是不变性,而酒店的营业时间就是validation. 一般使用规则和规约模式来实现validation和invariants。

规模模式: https://baijiahao.baidu.com/s?id=1717403406288752234&wfr=spider&for=pc


3 聚焦在行为,而不是属性状态

不要暴露属性给外面,如果外面得到属性,很可能就自己实现了一些领域逻辑,那么领域逻辑就外漏了。

4 把一些行为逻辑下方到值对象中

需要警惕实体逻辑膨胀,从而混绕了实体所要表达的概念。

例如预定是一个实体, 现在要加上逻辑预定的天数不能小于N天。 这个时候我们可以为Booking 抽象出 Stay 对象,让Stay对象去管理规则逻辑。 而不是让预定这个实体去做。 让预定只关注预定。


5 不要为世界建模

不要过度设计,只要满足需求就好。 不要让技术需求污染领域设计,除非真的万不得已。


6 分布式的设计

不需要让领域概念横跨多个bounded context, 如果我们域模型所涉及的概念横跨了,我们就需要用两种设计方法.

  1. 只是用id引用
  2. value objects



值对象

什么时候需要使用到值对象?

  • 1 概念需要凸显的时候。

e.g. 拍卖系统的能够一口价获取拍卖的价格, 就算是我们用一个int 就能表示也需要用类来凸显概念

public class WinningBid

{

...

public int Price { get; private set; }

...

}

  • 2 表述一个描述性的,但是没有实体编号的概念的时候。


值对象的特征

  • 无标识:他们只是标识对象的属性。
  • 基于属性的相等性: 所有的属性值相等即值对象相等
  • 富含行为:值对象实现业务概念的抽象,其也有自己的行为
  • 内聚:将不同的相关属性组成一个概念整体,例如Money, 是由一个long 和一个currency组成的
  • 不变性:值对象是不变的对象,如果需要改变属性,那最好是建立一个新的对象并且进行值对象替换。如果一定是需要改变,那就需要考虑设置为值对象是否合理。

不变性是值对象非常重要的一个属性,是可以保障值对象不会被"坏味道"代码侵入的一个原则之一。 例如如果一个值对象引入了另外一个类实例, 另外一个值对象也引入了相同的类实例, 如果值对象允许改动,当一个值对象对这个类实例的内容进行修改,势必会影响另外一个值对象。 所以最安全的方式还是通过对象替换的方式。



域服务

什么时候用域服务

发现和多个实体相关联,但是放入任何一个单独的实体都不适合,这个适合用域服务.


域服务应该包含什么内容

域服务应该包含业务/系统流程和业务规则,不应该包含技术的元素在内,技术的元素都应该在业务服务(Application Service)中实现。


应用服务与领域服务的区别:

一些可以在网上搜索到的老生常谈:

  • 应用服务里不要处理业务逻辑,只在领域服务里处理业务逻辑。(如何判断某段逻辑是否是业务逻辑?)
  • 领域服务掌握领域知识,而应用服务只是对领域服务的编排。
  • 应用服务是领域服务的客户方,也就是说应用服务会调用领域服务里的方法。
  • 当领域中的某个操作过程不属于实体或者值对象的职责时,需要将个操作放在领域服务中。而且确保领域服务是无状态的(这句话很有意思,也就是说领域服务中不应该有任何记录状态的行为,在任何情况下调用这个服务,它都不会有副作用,也就是说它是个纯内存操作)。
  • 领域服务中包含的是业务逻辑,而应用服务关注的应该是安全和事务等非业务逻辑。
  • 对事务的管理绝对不能放在领域服务层,事务管理需要放在应用服务层。因为和领域模型相关的操作的粒度都很细,无法用于事务管理。而且领域模型也不应该意识到事务的存在。
  • 通常的可以放在应用服务中的逻辑有:参数验证、错误处理、监控日志、事务处理、认证与授权。

除了第一条之外,上面的条例只是举出了应用服务与领域服务两者非常易于告之的差别,但是会有一些“业务逻辑”比较难以取舍, 例如

例如转账操作:

A ->B

A.accountDecrease(10);

B.accountIncrease(10);

我们在现在可以非常肯定的说上面的转账一定在领域服务,因为"转账"就是一个领域概念。 但是如果假设世面上所有的银行以前只有存钱和取钱两种功能,"转账"是一个新概念和业务的时候,技术就没有那么容易判别转账是一个临时性的一次性的需求,还是会长久发展。这个时候技术有两个选择:

1 构建转账域服务,让应用服务调用域服务

2 让应用服务获取A,B的实体,然后在应用层直接调用方法,在应用层做事务保持一致性。


如果域能力在其他团队手里,我相信大多数的团队会使用第二种。那遇到这种情况我们到底应该怎么办? 我个人的意见和阿里前技术高级专家张建飞在他的文章《一文教会你如何写复杂业务代码》的观点保持一致:

image.png

我们在新逻辑出现难以判断的时候优先讲能力放入到app层,如果我们发现会有第二个业务场景使用到了相同的能力,就需要考虑是否应该把此能力下发到领域层以增强内聚性和复用性。



工厂

  • 只负责复杂逻辑对象的构建,让构建逻辑中心化
  • 减少外部对象对构建对象内部变量的理解
  • 工厂方式不是在任何构建对象的时候使用,一定用在对象构建逻辑复杂,有子依赖或者是有invariant规则的场景。



Repository

reponsitory主要用来处理集合根的存储和获取的。提供一个facade接口且是面向domain层的,是domain model和data model的桥梁。其最大的作用就是通过反向依赖的方式充分隔离数据层和领域层。 Repository最常见的用法是被applicaiton service层去使用获取聚合根。

在repository实现中,我们一般会有下面的一些逻辑:

  • uniqe ID 的生成
  • 数据库的操作
  • 数据模型到领域模型的相互
  • 横跨多个数据模型构建出一个实体模型。


repository的反模式

  • 定义出比较通用化的接口, e.g.

 List<Customer> findBy(CusomterQuery query)

  • 使用了延迟加载,延迟加载就是设计错误的标志, 有可能说明我们聚合的边界不催。
  • 不要为了报表的诉求使用reponsitory, 领域的case和业务报告很不一样,可能需要多个聚合的数据,这种情况可以考虑用一个离线的store去做,和其他的读服务去做,不见得一定需要用领域的Repository模式。



领域事件

领域事件所想要解决的问题其实和metaQ消息机制想要解决的问题一致,都是跨领域驱动型业务逻辑实现的最佳方法,让领域和领域之前解耦。


领域事件消费教科书的说法是可以在领域层, 也可以在应用服务层, 但是我觉得领域消息如果用metaQ是这样的消息中间件去实现的话,那用领域层和应用服务层去消费就不是很方便,有可能破坏一些分层原则。 所以我个人倾向于在adapter层去承接消费,统一化掉。


当前可以实现领域事件和消费比较方便的工具有:

  • google guava - EventBus
  • COLA框架 - 事件支持



聚合/聚合根

聚合是什么?

其实聚合的原理和领域划分,限界上下文划分的原理是一致的,都是为了通过归类分组的方式让整个系统宏观上 N * N 的关系复杂度减低为 T * T 的复杂度。

T远小于N。

聚合前:

image.png

聚合后

image.png

如何划分聚合

1 根据业务规则和不变量来决定。 例如 customer 聚合是有一套业务规则来维持的,例如信用卡要存在,必须先有一个customer, 有customer 必须有address, address必须有code.  


2 强关联的对象应该放在一块。什么是强关联,那就是必须生命周期是一样的。 例如customer和creditcard在,电商网站中,如果customer被删除了,那么他的信用卡也应该被设置为失效的。  而订单和客户不一样,客户下了个订单,然后客户注销,但是订单还是一直存在的。所以customer 和 creditcard 在一个聚合中, 而用户和订单则不在。 订单里面可以有客户的ID或者是一个值对象。


3 灵活设置:有些可以根据业务情况,进行可以灵活的设置,下面列举一个论坛系统,帖子和回复聚合思考的例子:

大家都知道一个帖子有多个回复,没有帖子,回复就没有意义;所以很多人就会认为帖子应该聚合回复;但实际上不需要这样,如果你这样做了,那对于一个论坛来说,同一个帖子被多个人同时回复的可能性是非常高的,那这样的话,多个人同时回复一个帖子,就会导致多个人同时修改同一个帖子对象,那就导致大家都回复不了,因为会有并发冲突或者数据库事务的等待超时,因为大家都在修改同一个帖子聚合根;实际上如果我们从业务规则的角度去思考一下,那可以发现,其实帖子和回复之间,只有一个简单的规则,那就是回复一旦被创建,那他所对应的帖子不能被修改即可;这样的话,要实现这个规则其实很简单,把回复作为聚合根,然后把帖子传入回复聚合根的构造函数,然后回复保存帖子ID,然后回复将帖子ID设置为不允许外部修改(private set;即可),这样我们就实现了这个业务规则,同时还做到了多人同时推一个帖子回复时,不会对同一个帖子对象就并发修改,而是每个回复都是并行的往数据库插入一条回复记录即可。 -- 摘自阿里内部文档<<关于DDD领域驱动设计中聚合设计的一些思考>>

聚合设计的原则

  1. 聚合是用来封装真正的不变性,而不是简单的将对象组合在一起;
  2. 聚合应尽量设计的小;
  3. 聚合之间的关联通过ID,而不是对象引用;
  4. 聚合内强一致性,聚合之间最终一致性;


什么是聚合根

聚合根就是聚合的入口,聚合外部只能通过聚合根和聚合内部通信。 由于聚合外部只能通过聚合根和聚合内部通信, 这也就意味着外部不能操作除聚合根以外的任何类进行数据库操作,因为这样有可能会导致破坏业务的规则。举个例子,一个汽车四个轮子,如果我们用 Repo 直接操作轮子,对轮子采取delete,而这个时候汽车对象的状态却可能是 "running".


如何选出聚合根

1 聚合根一定至少有一个对应的datastore

2 聚合根一定更够完全描述一组后者多组业务规则(invariant)。 绝对不会存在一个业务规则需要多个聚合根联合作用才能做判断的。

3 聚合根一定有自己的独立的生命周期。



七 规范设计

项目/应用的规范设计的长期价值一定是不可忽视的,规范就相当于一个架构内部组件的收纳的容器,架构指定了这些组件在逻辑上的组织形式,但而规范则是则是妥妥的物理组织形式。如果没有合理清晰的规范设计,项目很快就会因为个人开发习惯的不同,导致物理结构混乱,给接下来的应用/项目的维护和扩展产生影响。


规范设计总共分为两类:

放对位置

  • 程序架构目录

贴好标签

  • 类名约定
  • 方法名约定
  • 错误码约定
  • Domain Event约定
  • 测试约定


下面是我们小团队参考了阿里COLA框架建议的命名规范做出的开发规范

程序架构组织

层次

包名

功能

Adapter层

web

处理页面请求的Controller

mtop

处理mtop的请求

hsf

处理hsf的请求

scheduler

处理定时器的请求

message

处理消息的请求

App层

executor

处理request,包括command和query

convertor

包含转换层类的目录

interceptor

包含拦截器目录

extpoint

扩展点目录

extention

扩展实现目录

handler

app 层的一些中间处理逻辑

Domain层

model

领域模型

ability

领域能力,包括DomainService

gateway

领域网关,解耦利器

extpoint

扩展点目录

extention

扩展实现目录

Infra层

gatewayimpl

网关实现

mapper

ibatis数据库映射

config

配置信息

Client SDK

api

服务对外透出的API

dto

服务对外的DTO


类命名规范

种类

对象

示例

API class

增删改服务入参

XXXCmd.

e.g. MetricsRegistrationCmd

查询服务入参

XXXQry.

e.g. ListAllMetricsQry

API service

XXXServiceI.java

e.g. MoMetricsAdminServiceI

出参

如果是无需返回返回使用Cola框架的Response,

如果是单个概念返回使用Cola框架的SingleResponse,

如果是多个概念返回使用Cola框架的MultiResponse.

如果概念是某个明确的对象,例如MultiResponse<MoMetricsCO>

如果是复合对象, 使用XXXResult 例如

MultiResponse<ListMetricsValuesResult>

领域层

实体

XXXE.java

值对象

XXXV.java

工厂

XXXFactory.java

Reponsitory

XXXRepository.java

域服务

XXXDomainService.java

防腐服务

XXXGateway.java

枚举

XXXEnum.java

应用层

命令执行器

XXXXCmdExe.java

查询执行器

XXXXQueryExe.java

拦截器

XXXInterceptor.java

扩展接口

XXXExtPt.java

扩展实现

XXXExt.java

e.g.

#{Biz}#{userCase}#{scenario}Ext.java

转换器

XXXConvertor.java

基础设施层

存储的对象

XXXPO.java

仓库实现

XXXReponsitoryImpl.java

防腐服务实现

XXXGatewayImpl.java

公共层

工具类

XXXUtil.java

ThreadLocal

XXXThreadlocal.java

其他

接口

XXXI.java

Spring配置类

XXXConfiguration.java

配置属性聚合类

XXXProperties.java


错误码规范

image.png


扩展规范

使用COLA的扩展框架去实现。COLA的扩展框架的功能不在赘诉,这里讨论的是COLA框架的BizScenario的划分。

BizScenario

private String bizId = "#defaultBizId#";

private String useCase = "#defaultUseCase#";

private String scenario = "#defaultScenario#";

bizId: 业务,对应某个业务诉求, e.g. 新品孵化。

useCase: 业务用例, e.g. 策略定义

scenario: 用例下的某个场景。 e.g. 定义商家事件任务, 定义小二事件任务

全局业务通用实现

@Extention

某项业务通用实现

@Extention(bizId=XXX)

某项业务,某个用例的通用实现

@Extention(bizId=XXX, userCase=YYY)

某个具体实现

@Extention(bizId=XXX, userCase=YYY,scenario=ZZZ)


参考

【1】「技术人生」专题第1篇:什么是技术一号位?

【2】https://time.geekbang.org/column/article/6605

【3】https://baike.baidu.com/item/%E9%97%AE%E9%A2%98%E5%9F%9F/7181289?fr=aladdin

【4】实现领域驱动设计. 电子工业出版社

【5】https://github.com/alibaba/COLA

目录
相关文章
|
7月前
|
存储 测试技术 数据库
谈谈代码:降低复杂度,从放弃三层架构到DDD入门
最近我发现团队某项目的复杂度越来越高(典型的三层架构),具体表现为: - 代码可读性较差:各个服务之间调用复杂,流程不清晰 - 修改某服务业务代码导致大量无关服务的测试用例失败,单个功能开发者很难迅速定位相关问题 - 测试用例特别难编写,需要mock大量数据来拉起整块服务
245 4
谈谈代码:降低复杂度,从放弃三层架构到DDD入门
|
4月前
|
存储 Java 数据库连接
成为工程师 - 系统分层的设计原则
成为工程师 - 系统分层的设计原则
|
4月前
|
开发者
软件设计与架构复杂度问题之McCabe圈复杂度的定义如何解决
软件设计与架构复杂度问题之McCabe圈复杂度的定义如何解决
|
4月前
|
BI
软件设计与架构复杂度问题之业务简单的系统不适合使用DDD架构如何解决
软件设计与架构复杂度问题之业务简单的系统不适合使用DDD架构如何解决
|
4月前
|
设计模式
软件设计与架构复杂度问题之认知负荷的定义如何解决
软件设计与架构复杂度问题之认知负荷的定义如何解决
|
4月前
|
Serverless 微服务
软件设计与架构复杂度问题之ady Booch描述软件的复杂性如何解决
软件设计与架构复杂度问题之ady Booch描述软件的复杂性如何解决
|
4月前
|
微服务
软件设计与架构复杂度问题之理解软件复杂性的递增性如何解决
软件设计与架构复杂度问题之理解软件复杂性的递增性如何解决
|
4月前
|
开发者
软件设计与架构复杂度问题之注释在软件设计中的角色如何解决
软件设计与架构复杂度问题之注释在软件设计中的角色如何解决
|
4月前
|
测试技术
软件设计与架构复杂度问题之区分软件维护、演进和保护(苟且)如何解决
软件设计与架构复杂度问题之区分软件维护、演进和保护(苟且)如何解决
|
4月前
|
程序员
软件设计与架构复杂度问题之战略编程与战术编程的主要区别如何解决
软件设计与架构复杂度问题之战略编程与战术编程的主要区别如何解决