领域建模和设计的重要性:
Eric Evans将其定义为领域驱动设计(Domain-Driven Design,简称DDD) 方法论
服务模块之间过渡耦合
随着迭代的不断演化,业务逻辑变得越来越复杂,我们的系统也越来越冗杂。模块彼此关联,谁都很难说清模块的具体功能意图是啥。例如:
订单服务模块中 具有以下接口 订单存储(数据表)
订单接口 订单字段
评价接口 评价字段
支付接口 支付相关字段
保险接口 保险相关字段等等。。。
订单服务模块中的订单接口中提供了查询、创建订单相关的接口,也提供了订单评价、支付、保险的接口。
同时我们的表也是一个订单大表,包含了非常多字段。
在我们维护代码时,牵一发而动全身,很可能只是想改下评价相关的功能,却影响到了创单核心路径。
虽然我们可以通过测试保证功能完备性,但当我们在订单领域有大量需求同时并行开发时,改动重叠、恶性循环、疲于奔命修改各种问题。
上述问题,归根到底在于系统架构不清晰,划分出来的模块内聚度低、高耦合。 解决方案:
按照演进式设计的理论:让系统的设计随着系统实现的增长而增长。我们不需要作提前设计,就让系统伴随业务成长而演进。这当然是可行的,敏捷实践中的重构、测试驱动设计及持续集成可以对付各种混乱问题。
重构——保持行为不变的代码改善清除了不协调的局部设计,测试驱动设计确保对系统的更改不会导致系统丢失或破坏现有功能,持续集成则为团队提供了同一代码库。
在这三种实践中,重构是克服演进式设计中大杂烩问题的主力,通过在单独的类及方法级别上做一系列小步重构来完成。我们可以很容易重构出一个独立的类来放某些通用的逻辑,但是你会发现你很难给它一个业务上的含义,只能给予一个技术维度描绘的含义
重构可以解决问题,但是对新同学不太友好,新同学并不总是知道对通用逻辑的改动或获取来自该类。显然,制定项目规范并不是好的idea。我们又闻到了代码即将腐败的味道。
领域模型:则表达与业务相关的事实(从业务中抽象出领域模型),将数据和行为封装在一起 贫血模型(失血模型)和充血模型介绍
贫血领域对象(贫血模型)
贫血领域对象(Anemic Domain Object)只是仅用作数据载体,而没有行为和动作的领域对象。
失血模型简单来说,就是domain object只有属性的getter/setter方法的纯数据类,所有的业务逻辑完全由business object来完成,这种模型下的domain object被Martin Fowler称之为“贫血的domain object”
过程式代码:
习惯了三层接口开发 mvc模式(Action/Service/DAO这种分层模式),使用这种开发方式,对象只是数据的载体,没有行为。以数据为中心,以数据库ER设计作驱动。 分层架构在这种开发模式下,可以理解为是对数据移动、处理和实现的过程。
举例: 抽奖平台设计: 场景需求 奖池里配置了很多奖项,我们需要按运营预先配置的概率抽中一个奖项。 实现非常简单,生成一个随机数,匹配符合该随机数生成概率的奖项即可。
奖项
奖池
可以发现:在业务领域里非常重要的抽奖,我的业务逻辑都是写在Service中的,Award充其量只是个数据载体,没有任何行为。
简单的业务系统采用这种贫血模型和过程化设计是没有问题的,但在业务逻辑复杂的情况下,业务逻辑、状态会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症。 由贫血症 引起的失忆症
用领域模型的开发方式,将数据和行为封装在一起,并与现实世界中的业务对象相映射。各类具备明确的职责划分,将领域逻辑分散到领域对象中。继续举我们上述抽奖的例子,使用概率选择对应的奖品就应当放到AwardPool类中。
充血模型:
系统困境与软件复杂度,为什么我们的系统会如此复杂?
解决复杂和大规模软件的武器可以被粗略地归为三类:抽象、分治和知识。
抽象: 使用抽象能够精简问题空间,而且问题越小越容易理解。举个例子,从北京到上海出差,可以先理解为使用交通工具前往,但不需要一开始就想清楚到底是高铁还是飞机,以及乘坐他们需要注意什么。
分治: 把问题空间分割为规模更小且易于处理的若干子问题。分割后的问题需要足够小,以便一个人单枪匹马就能够解决他们;
其次,必须考虑如何将分割后的各个部分装配为整体。分割得越合理越易于理解,在装配成整体时,所需跟踪的细节也就越少。即更容易设计各部分的协作方式。评判什么是分治得好,即高内聚低耦合。
知识 顾名思义,DDD可以认为是知识的一种。自身的知识储备,可以让我们在面临问题是选择出合适的技术去解决问题
DDD提供了这样的知识手段,让我们知道如何抽象出限界上下文以及如何去分治。
DDD 的限界上下文与微服务架构相得益彰
微服务架构众所周知,此处不做赘述。我们创建微服务时,需要创建一个高内聚、低耦合的微服务。而DDD中的限界上下文则完美匹配微服务要求,可以将该限界上下文理解为一个微服务进程。
分治: 实现系统复杂度的拆分,一般有两种方式: 技术维度 业务维度
技术维度:类似于MVC
业务维度:则按照业务领域来划分系统
微服务架构和DDD同样注重业务视角
我们将架构设计活动精简为以下三个层面:
业务架构——根据业务需求设计业务模块及其关系
系统架构——设计系统和子系统的模块
技术架构——决定采用的技术及框架
DDD的核心诉求就是将业务架构映射到系统架构上,在响应业务变化调整业务架构时,也随之变化系统架构。而微服务追求业务层面的复用,设计出来的系统架构和业务一致;
在技术架构上则系统模块之间充分解耦,可以自由地选择合适的技术架构,去中心化地治理技术和数据。
DDD的专业术语:
贫血模型
充血模型
战略建模
战术设计
应用服务--》领域服务 --》通过资源库获取聚合根
--》通过资源库持久化聚合根
--》发布领域事件
=================================================================================================================
为什么领域驱动设计
一种思维方式
如何领域驱动设计
战略设计 --------》(从整体到局部)----------》 战术设计
战略设计 (如何进行战略设计)
1.通用语言(统一语言):
定义:通过统一语言 ,提炼出领域知识的产出物, 体现在两个方面:统一的领域术语 领域行为描述
如何获取统一语言: 统一语言就是需求分析的过程,也就是团队中各个角色就系统目标 范围 和具体的功能达成一致性的过程
输出: 概念 英文 定义 约束(避免快速腐化) 举例
2.领域:问题 + 边界 + 知识
领域包含了问题域和解系统。一般认为软件是对现实世界的部分模拟。在DDD中,解系统可以映射为一个个限界上下文,限界上下文就是软件对于问题域的一个特定的、有限的解决方案。
子域和限界上下文(分治,解决系统复杂性)
子域划分:确定逻辑边界
核心域
通用域
支撑域
限界上下文:(一个具有边界的,解决特定问题的解决方案)
一个由显示边界限定的特定职责。领域模型便存在于这个边界之内。在边界内,每一个模型概念,包括它的属性和操作,都具有特殊的含义。
一个给定的业务领域会包含多个限界上下文,想与一个限界上下文沟通,则需要通过显示边界进行通信。
系统通过确定的限界上下文来进行解耦,而每一个限界上下文内部紧密组织,职责明确,具有较高的内聚性。
一个很形象的隐喻:细胞质所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。
如何划分出一个限界上下文? 限界上下文应该从需求出发,按领域划分
显然我们不应该按技术架构或者开发任务来创建限界上下文,应该按照语义的边界来考虑。
实践方案:考虑产品所讲的通用语言,从中提取一些术语称之为概念对象,寻找对象之间的联系; 从需求中的名词 抽象出 概念对象,并试图找寻对象之间的关系
或者从需求里提取一些动词,观察动词和对象之间的关系; 从需求中动词,抽象出行为
我们将紧耦合的各自圈在一起,观察他们内在的联系,从而形成对应的界限上下文。
形成之后,我们可以尝试用语言来描述下界限上下文的职责,
看它是否清晰、准确、简洁和完整。简言之,限界上下文应该从需求出发,按领域划分
3.上下文映射图:关联各个上下文
在进行上下文划分之后,我们还需要进一步梳理上下文之间的关系。
康威(梅尔·康威)定律
任何组织在设计一套系统(广义概念上的系统)时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。
梳理清楚上下文之间的关系,从团队内部的关系来看,有如下好处:
任务更好拆分,一个开发人员可以全身心的投入到相关的一个单独的上下文中;
沟通更加顺畅,一个上下文可以明确自己对其他上下文的依赖关系,从而使得团队内开发直接更好的对接。
从团队间的关系来看,明确的上下文关系能够带来如下帮助:
每个团队在它的上下文中能够更加明确自己领域内的概念,因为上下文是领域的解系统;
对于限界上下文之间发生交互,团队与上下文的一致性,能够保证我们明确对接的团队和依赖的上下游。
限界上下文之间的映射关系
1.合作关系(Partnership)PS:两个上下文紧密合作的关系,一荣俱荣,一损俱损。
2.共享内核(Shared Kernel)SK:两个上下文依赖部分共享的模型。
3.客户方-供应方开发(Customer-Supplier Development)CSD:上下文之间有组织的上下游依赖。
4.遵奉者(Conformist):下游上下文只能盲目依赖上游上下文。
5.防腐层(Anticorruption Layer)ACL:一个上下文通过一些适配和转换与另一个上下文交互。
6.开放主机服务(Open Host Service)OHS:定义一种协议来让其他上下文来对本上下文进行访问。
7.发布语言(Published Language)PL:通常与OHS一起使用,用于定义开放主机的协议。
8.大泥球(Big Ball of Mud):混杂在一起的上下文关系,边界不清晰。
9.另谋他路(SeparateWay):两个完全没有任何联系的上下文。
战术设计:设计过程
概念建模(面向业务 问题 业务 抽象) ----------》 领域建模(概念到代码的一个过渡)------------》 框架设计(代码设计 解决 技术 具体)
框架设计的总体思路:分层 CQRS EDA
用户接口层 : web app api mq
应用层
领域层 实体 值 聚合根
基础设施层 base org upm gis
核心概念:
实体
当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。
例:最简单的,公安系统的身份信息录入,对于人的模拟,即认为是实体,因为每个人是独一无二的,且其具有唯一标识(如公安系统分发的身份证号码)。
在实践上建议将属性的验证放到实体中。
值对象
当一个对象用于对事务进行描述而没有唯一标识时,它被称作值对象(Value Object)。
例:比如颜色信息,我们只需要知道{“name”:“黑色”,”css”:“#000000”}这样的值信息就能够满足要求了,这避免了我们对标识追踪带来的系统复杂性。
值对象很重要,在习惯了使用数据库的数据建模后,很容易将所有对象看作实体。使用值对象,可以更好地做系统优化、精简设计。
它具有不变性、相等性和可替换性。
在实践中,需要保证值对象创建后就不能被修改,即不允许外部再修改其属性。在不同上下文集成时,会出现模型概念的公用,
如商品模型会存在于电商的各个上下文中。在订单上下文中如果你只关注下单时商品信息快照,那么将商品对象视为值对象是很好的选择。
聚合根
Aggregate(聚合)是一组相关对象的集合,作为一个整体被外界访问,聚合根(Aggregate Root)是这个聚合的根节点。
聚合是一个非常重要的概念,核心领域往往都需要用聚合来表达。其次,聚合在技术上有非常高的价值,可以指导详细设计。
聚合
聚合由根实体,值对象和实体组成。
资源库
领域对象需要资源存储,存储的手段可以是多样化的,常见的无非是数据库,分布式缓存,本地缓存等。资源库(Repository)的作用,就是对领域的存储和访问进行统一管理的对象。
生命周期:
聚合(Aggregate)
聚合就是一组应该呆在一起的对象,聚合根(Aggregate Root)就是聚合在一起的基础,并提供对这个聚合的操作。聚合除了聚合根以外,还有自己的边界(boundary),即聚合里有什么。
例如:一个订单可以有多个订单明细,订单明细不可能脱离订单而存在,而订单也不可能没有订单明细。
这种情况下,订单和订单明细就是一个聚合,而订单就是这个聚合的聚合根,订单和订单明细就处于这个聚合的边界之内。
如果要变更订单明细,我们需要通过操作聚合根订单来实现,如order.changeItemCount(),而非订单明细自身。
另外一个例子:一名客户可以有多个订单,订单不可能脱离客户而存在,而客户却可以没有订单。
这种情况下,客户和订单就是不同的两个聚合,一个聚合以客户为聚合根,另一个聚合以订单为聚合根,引用客户的标识。
客户里并不引用订单的标识,这样将关联减至最少有助于简化对象的关系网。但是带来的一个麻烦就是如果要查找某位客户的所有订单,就不得不从所有的订单里查,而不能从客户这个聚合里直接获得。
最后再举一个多对多的例子:一个班级可以有多名学生,学生可以脱离这个班级而存在,而班级不能没有学生,学生也不能不在班级里 。
这种情况下,班级和学生也是不同的两个聚合,一个聚合以班级为聚合根,引用学生的标识;另一个聚合以学生为聚合根,引用班级的标识,将多对多转换成两个一对多。
聚合是持久化的一个单位,我们需要保证以聚合为单位的数据一致性。如果聚合太大,那就会导致并发修改困难,多人并发修改同一个聚合里的不同项目,
结果就是只有第一个提交的人成功修改,其它人不得不重新刷新聚合才能再次修改。大聚合还会导致性能问题,因为操作实体时会将整个大聚合同时加载进内存。珍爱生命,拒绝大聚合。
聚合根必须是实体而非值对象,因为它需要整体持久化,所以一定会有标识。
而聚合根里的各个元素,既可能是实体,也可能是值对象。例如:一个订单(聚合根)一般会有订单明细(实体)和送货地址(值对象)。
这些元素里可以有对聚合根的引用,但是不能相互引用。任何对其它元素的操作都必须通过聚合根来进行。聚合根里的标识是全局的,
聚合根里的实体标识是聚合里唯一的本地标识,因为对它的访问都是通过聚合根来操作的。聚合根拥有自己独立的生命周期,其实体的生命周期从属于其所属的聚合,值对象因为只是值而已,并没有生命周期。
工厂(Factory)
工厂是生命周期的开始阶段,它可以用来创建复杂的对象或是一整个聚合。复杂对象的创建是领域层的职责,但它并不属于被创建的对象自身的职责。实体和值对象的工厂不太一样,因为值对象是不可变的,
所以需要工厂一次性创建一个完整的值对象出来。而实体工厂则可以选择创建之后再补充一些细节。
资源库(Repository)
资源库是生命周期的结束,它封装了基础设施以提供查询和持久化聚合的操作。
这样能够让我们始终聚焦于模型,而把对象的存储和访问都委托给资源库来完成。
以订单和订单明细的聚合为例,因为一定是通过订单这个聚合根来获取订单明细,所以可以有订单的资源库,但是不能有订单明细的资源库。
也就是说,只有聚合才拥有资源库。需要注意的是,资源库并不是数据库的封装,而是领域层与基础设施之间的桥梁。
DDD关心的是领域内的模型,而并非是数据库的操作。理想的资源库对客户(而非开发者)隐藏了内部的工作细节,委托基础设施层来干那些脏活,到关系型数据库、NOSQL、甚至内存里读取和存储数据。
什么是复杂? 如何定义复杂?
从理解力 和 预测能力 来个维度来分析 复杂系统理论
抽象 分解 层次结构
如何划分 领域服务 和 应用服务
战略层语境:
领域服务通常指 相对聚焦的底层支撑域、通用域服务
应用服务通常指面向业务场景的负责功能组装的服务
战术层语境:
领域服务指 领域建模工具中所指的 领域服务
应用服务指面向场景的技术实现组装
DDD对 clean code的再定义
统一语言 与统一语言英文保持一致性的代码命名
Domain层: domain层 仅包含领域模型定义的对象,且用 plain object
不依赖spring ICO 和APO等第三方包
拒绝对getset以及构造方法进行注解
拒绝setter update modify save delete 等无法明确业务含义的方法
值对象不用加上标识技术语言的Enun
应用层: application层拒绝xxxxHandle xxxxProcessor xxxxContext
区分命令和查询 命令推荐xxxxCommandService 查询推荐xxxxQueryService
infrastructure层 基础支撑层
资源库repository的 入参和出擦 除了原始数据类型,只能包含领域对象
Repository 对象交互拒绝DTO PO
对外接口访问的防腐层,统一命名为xxxAdaptor
禁止外部接口对象直接上层透传
事件
事件命名为 事件+Event 且事件命名为动词过去式