最近加入一个DDD+低代码平台低项目,元数据多租驱动编程,让软件实现批量低成本可复制。
目前国内外这个领域不可谓不倦,salesforce、微软、 Pega、国内阿里、华为、用友、金蝶等都软件厂商趋之若素。
低代码平台如果不结合DDD和元数据多租,那么就是水中楼阁。
今天总结下DDD领域编程
DDD的作用
统一思想:统一项目各方业务、产品、开发对问题的认知,而不是开发和产品统一,业务又和产品统一从而产生分歧。
明确分工:域模型需要明确定义来解决方方面面的问题,而针对这些问题则形成了团队分钟的理解。
反映变化:需求是不断变化的,因此我们的模型也是在不断的变化的。领域模型则可以真实的反映这些变化。
边界分离:领域模型与数据模型分离,用领域模型来界定哪些需求在什么地方实现,保持结构清晰。
DDD的基本概念
领域驱动设计围绕着领域模型进行设计,通过分层架构将领域独立出来。
这里有两个关键词:
领域模型
分层架构
领域模型的对象包括:实体、值对象和领域服务,领域逻辑都应该封装在这些对象中。
领域模型 Domain Model
领域反映到代码里就是模型,模型是对领域某个方面的抽象,并且可以用来解决相关域的问题,
模型分为实体和值对象两种。
实体对象 Entities
有唯一标志的核心领域对象,且这个标志在整个软件生命周期中都不会发生变化。
这个概念和我们平时软件模型中和数据库打交道的Entity实体比较接近,
不同的是DDD中这些实体会包含与该实体相关的业务逻辑,它是操作行为的载体。
实体 = 唯一身份标识 + 可变性【状态 + 行为】
DDD 中要求实体是唯一的且可持续变化的。
意思是说在实体的生命周期内,无论其如何变化,其仍旧是同一个实体。
唯一性由唯一的身份标识来决定的。
可变性也正反映了实体本身的状态和行为。
实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。
我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。
但是,由于它们拥有相同的 ID,它们依然是同一个实体。
比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。
值对象 Value Object
依附于实体存在,通过对象属性来识别的对象,它将一些相关的实体属性打包在一起处理,形成一个新的对象。
这些对象是用来表示临时的事物,或者可以认为值对象是实体的属性,这些属性没有特性标识但同时表达了领域中某类含义的概念。
通常值对象不具有唯一id,由对象的属性描述,可以用来传递参数或对实体进行补充描述。
聚合
实体和值对象表现的是个体的能力,而我们的业务逻辑往往很复杂,依赖个体是无法完成的,这时候就需要多个实体和值对象一起协同工作,而这个协同的组织就是聚合。
聚合是数据修改和持久化的基本单元,同一个聚合内要保证事务的一致性,所以在设计的时候要保证聚合的设计拆分到最小化以保证效率和性能。
聚合根
也叫做根实体,一个特殊的实体,它是聚合的管理者,代表聚合的入口,抓住聚合根可以抓住整个聚合。
领域服务
有些领域的操作是一些动词,并不能简单的把他们归类到某个实体或者值对象中。
领域的动作,从领域中识别出来之后,应该将它声明成一个服务,它的作用仅仅是为领域提供相应的功能。
简单理解: 就是业务方法。
领域事件
在特定的领域由用户动作触发,表示发生在过去的事件,或者领域状态的变化。
领域事件
在特定的领域由用户动作触发,表示发生在过去的事件,或者领域状态的变化。
失血模型
模型中只有简单的get set方法,是对一个实体最简单的封装,其他所有的业务行为由服务类来完成。
@Data
@ToString
public class User {
private Long id;
private String username;
private String password;
private Integer status;
private Date createdAt;
private Date updatedAt;
private Integer isDeleted;
}
而且,其他所有的业务行为由服务类来完成,
public class UserService{
public boolean isActive(User user){
return user.getStatus().equals(StatusEnum.ACTIVE.getCode());
}
}
这个就是我们如常编程中最常用都编码模型
贫血模型
贫血模型是指领域对象里只有get和set方法(POJO),所有的业务逻辑都不包含在内而是放在Business Logic层。
@Data
@ToString
public class User {
private Long id;
private String username;
private String password;
private Integer status;
private Date createdAt;
private Date updatedAt;
private Integer isDeleted;
public boolean isActive(User user){
return user.getStatus().equals(StatusEnum.ACTIVE.getCode());
}
public void setUsername(String username){
return username.trim();
}
}
贫血模型在失血模型基础之上,领域对象的包含一些状态变化,但是停留在内存层面,不关心数据持久化。
贫血模型所有的业务逻辑都不包含在内而是放在Business Logic层。
贫血模型优点是系统的层次结构清楚,各层之间单向依赖,Client->(Business Facade)->Business Logic->Data Access Object。
可见,领域对象几乎只作传输介质之用,不会影响到层次的划分。
这就是 传统的 数据驱动的开发。
在使用Spring的时候,通常暗示着你使用了贫血模型,我们把Domain类用来单纯地存储数据,Spring管不着这些类的注入和管理,Spring关心的逻辑层(比如单例的被池化了的Business Logic层)可以被设计成singleton的bean。
假使我们这里逆天而行,硬要在Domain类中提供业务逻辑方法,那么我们在使用Spring构造这样的数据bean的时候就遇到许多麻烦,比如:bean之间的引用,可能引起大范围的bean之间的嵌套构造器的调用。
充血模型
在贫血模型基础上,负责数据的持久化。
@Data
@ToString
public class User {
private Long id;
private String username;
private String password;
private Integer status;
private Date createdAt;
private Date updatedAt;
private Integer isDeleted;
private UserRepository userRepository;
public boolean isActive(User user){
return user.getStatus().equals(StatusEnum.ACTIVE.getCode());
}
public void setUsername(String username){
this.username = username.trim();
userRepository.update(user);
}
}
充血模型层次结构和上面的差不多,不过大多业务逻辑放在Domain Object里面,Business Logic只是简单封装部分业务逻辑以及控制事务、权限等,这样层次结构就变成Client->(Business Facade)->Business Logic->Domain Object->Data Access Object。
它的优点是面向对象,Business Logic符合单一职责,不像在贫血模型里面那样包含所有的业务逻辑太过沉重。
胀血模型
service都不需要,所有的业务逻辑、数据存储都放到一个类中。
对于DDD来说,失血和胀血都是不合适的,
失血太轻量没有聚合,胀血那是初学者才这样写代码。
那么充血模型和贫血模型该怎么选择?
充血模型依赖repository接口,与数据存储紧密相关,有破坏程序稳定性的风险。
DDD建模方法
用例分析法
四色建模法
事件风暴法
用例分析法
用例分析法是领域建模最简单可行的方式。
大致可以分为获取用例、收集实体、添加关联、添加属性、模型精化几个步骤。
四色建模法
四色建模法源于《Java Modeling In Color With UML》,它是一种模型的分析和设计方法,
通过把所有模型分为四种类型,帮助模型做到清晰、可追溯。
简单来说,四色关注的是某个人的角色在某个地点的角色用某个东西的角色做了某件事情。
Peter Coad和Mark Mayfield奠定了4种架构型(一种形式,所有的东西都或多或少地遵守)的早期工作。
事件风暴法
事件风暴法类似头脑风暴,简单来说就是谁在何时基于什么做了什么,产生了什么,影响了什么事情。
DDD分层架构
DDD的概念
领域驱动设计围绕着领域模型进行设计,通过分层架构将领域独立出来。
这里有两个关键词:
领域模型
分层架构
领域模型的对象包括:实体、值对象和领域服务,领域逻辑都应该封装在这些对象中。
聚合是一种边界,它可以封装一到多个实体与值对象,并维持该边界范围之内的业务完整性。在聚合中,至少包含一个实体,且只有实体才能作为聚合根。工厂和资源库都是对领域对象生命周期的管理。工厂负责领域对象的创建,用于封装复杂或者可能变化的创建逻辑。资源库负责从存放资源的持久层获取、添加、删除或者修改领域对象。
传统的三层架构
传统的MVC模型把框架分成了三层:显示层、控制层、模型层。
显示层负责显示用户界面,控制层负责处理业务逻辑、而模型则负责与数据库通信,对数据进行持久化的操作。
从代码角度来看,这样的框架结构每个模块职责分离,特别适合小型的应用系统。
最简单的分层方式自然就是“表现层、业务逻辑层和数据访问层”,如下图:
其中包含一些 帮助类/工具类,比如 SQLHelper、StringUtility之类,归纳到 基础结构层。
在传统的三层架构中,表现层只能跟业务逻辑层打交道,而业务逻辑层 只能跟 数据访问层 打交道。
这也是一般的编程规范都要求: 表现层不能访问 DAO层,表现层对数据访问层的内容一无所知。
从领域驱动的角度看,这种分层的方式有一定的弊端。
主要有两点:
首先, 基础结构层作为 公共组件,为各个层面提供服务,职责职责比较紊乱。
基础结构层 既可以是纯粹的技术框架,又可以包含或处理一定的业务逻辑,这样一来,业务逻辑层与“基础结构层”之间就会存在依赖关系;
其次,传统的三层架构数据表驱动编程,或者可以说是面向数据表编程,总之,这种结构过分地突出了“数据访问”的地位。
(其实目前我所在组织也是数据驱动元数据 再面向元数据编程)
这种面向数据表模式中,“数据访问”甚至超过了 “业务逻辑”地位。一旦在数据访问弱化的场景中,甚至不存在库表的场景中,很多软件人员一上来就问:“我没有表,难道还要三层?不用三层,该怎么办? 这就问题来了。
另外,随着业务复杂度的上升,会发现服务层的逻辑以及代码不断增长,变得庞大且复杂、测试成本直线上升。由于各个Service的逻辑散落在各处,后续新需求的维护的成本也非常高,导致交付效率越来越低,稳定性风险也越来越高。
除了日常开发的一些问题外,由于缺乏一定的业务知识沉淀,如果文档沉淀或者更新不及时的情况下,新同学来接受一个新的小需求,面对产品描述的需求改动点,开发同学根本无从下手。一方面是新同学业务的生疏,但真正根本原因还是:开发与产品之间的语言不能保持一致,双方对于同一事物的表达和理解有很大的区别。产品描述的更多是实际的业务场景,而开发则更关注背后的具体实现逻辑,加之文档的缺失,可以说是面对一堆模型和代码两眼茫然。
怎么办?DDD来解决这摊子问题。
DDD领域驱动设计的分层架构
DDD将软件系统分为四层:基础结构层、领域层、应用层和表现层。
在**《领域驱动设计——软件核心复杂性的应对之道》**一书中,DDD 范式的创始人 Evans 提出下图所示的这样一种分层架构:
整个系统划分为:
基础设施层(Infrastructure)
领域层(Domain)
应用层(Application)
表示层(也称为 用户接口层 User Interface)
与上述的三层相比,**数据访问层(DAO层)**已经不在了,它被移到基础结构层了。
基础结构层 (Infrastructure Layer):
该层专为其它各层提供技术框架支持。
基础结构层的基本原则:不会涉及任何业务知识,与业务无关,不涉及业务逻辑。
为啥数据访问的内容移动到了基础结构层 ?因为 数据的CRUD,本质上不涉及业务逻辑,或者说数据的读写是业务无关的。所以,数据访问的内容,也被放在了该层当中。
Infrastructure Layer 一些例子:
领域层需要持久化服务,在DDD中,领域层通过仓储(Repository)接口定义持久化需求,基础设施层通过采用JDBC、JPA、Hibernate、NoSQL等技术之一实现领域层的仓储接口,为领域层提供持久化服务。
领域层需要消息通知服务,在领域层中定义了一个NotificationService领域服务接口,基础设施层通过采用手机短信、电子邮件、Jabber等技术实现NotificationService领域服务接口,为领域层提供消息通知服务。
用户接口层需要一个对象序列化服务,将任何JavaBean序列化为JSON字符串,可以在用户接口层定义一个ObjectSerializer服务接口,基础设施层通过采用Gson实现这一接口,为用户接口层提供对象序列化服务。
领域层 (Domain Layer):
包含了业务所涉及的领域对象(实体、值对象)、领域服务以及它们之间的关系。
这部分内容的具体表现形式是: 领域模型(Domain Model)。
DDD提倡富领域模型,即尽量将业务逻辑归属到领域对象上,实在无法归属的部分,则以领域服务的形式进行定义。
什么是业务逻辑?
业务逻辑就是存在于问题域即业务领域中的实体、概念、规则和策略等,业务逻辑与软件实现无关,主要包含下面的内容:
业务实体(领域对象)。例如银行储蓄领域中的账户、信用卡等等业务实体。
业务规则。例如借记卡取款数额不得超过账户余额,信用卡支付不得超过授信金额,转账时转出账户余额减少的数量等于转入账户余额增加的数量,取款、存款和转账必须留下记录,等等。
业务策略。例如机票预订的超订策略(卖出的票的数量稍微超过航班座位的数量,以防有些旅客临时取消登机导致座位空置)等。
完整性约束。例如账户的账号不得为空,借记卡余额不得为负数等等。本质上,完整性约束是业务规则的一部分。
业务流程。例如,“在线订购”是一个业务流程,它包括“用户登录-选择商品-结算-下订单-付款-确认收货”这一系列流程。
对领域层的进一步说明如下:
领域层实现映射到领域模型,是设计维度的领域模型(Domain Model)在软件中的具体实现。
包含实体、值对象和领域服务等领域对象,通常这些领域对象和问题域中的概念实体一一对应,具有相同或相似的属性和行为。
在实体、值对象和领域服务等领域对象的方法中,封装实现业务规则和保证完整性约束(这一点是与CRUD模式相比最明显的差别,CRUD中的领域对象没有行为)。
领域对象在实现业务逻辑上具备坚不可摧的完整性,意味着不管外界代码如何操作,都不可能创建不合法的领域对象(例如没有账户号码或余额为负数的借记卡对象),亦不可能打破任何业务规则(例如在多次转账之后,钱凭空丢失或凭空产生)。
领域对象的功能是高度内聚的,具有单一的职责,任何不涉及业务逻辑的复杂的组合操作都不在领域层而在应用层中实现。
领域层中的全部领域对象的总和在功能上是完备的,意味着系统的所有行为都可以由领域层中的领域对象组合实现。
应用层/工作流层 (Application Layer):
应用层是领域驱动中最有争议的一个层次,也会有很多人对其职责感到模糊不清。
应用层不包含任何领域逻辑,但它会对任务进行协调,并可以维护应用程序的状态,
因此,应用层更注重流程性的东西。在某些领域驱动设计的实践中,也会将其称为“工作流层”。
既然不包含领域逻辑,那应用层又如何协调工作任务呢?
它通过排列组合领域层的领域对象来实现用例,它的职责可表示为“编排和转发”,
具体来说,Application Layer 将要实现的功能委托给一个或多个领域对象来实现,Application Layer 只负责安排工作顺序和拼装操作结果。
如果一定要进行类比的话 ,Application Layer 的职责 类似于微服务领域的 SpringCloud gateway,或者服务总线Service Bus。
低代码平台更是热衷于将工作流引擎和微服务编排引擎还有前端UI编排引擎结合起来,
图形化编排调度成了IT界的卷王。因其在减少IT投入和IT系统可复制上的优势。
表现层/用户接口层(User Interface):
这个好理解,跟三层架构里的表现层(Controller)意思差不多,
表现层依赖于应用层,但是表现层与应用层之间是通过数据传输对象(DTO)进行交互的,
DTO数据传输对象是没有行为的POCO(C#中的概念)对象,DTO的目的只是为了对领域对象(Domain Object)中的数据进行封装,剥离了Domain Object中的行为,实现层与层之间的数据传递。
为何不能直接将 Domain Object 用于数据传递?两个原因:
(1) 因为Domain Object 更注重领域,而DTO更注重数据。
(2) 由于“富领域模型”的特点,这样做会直接将Domain Object 的行为暴露给表现层。
表现层为外部用户访问底层系统提供交互界面和数据表示。
表现层在底层系统之上封装了一层可访问外壳,为特定类型的外部用户(人或计算机程序)访问底层系统提供访问入口,并将底层系统的状态数据以该类型客户需要的形式呈现给它们。
表现层有两个任务:
(1)从用户处接收命令操作,改变底层系统状态;
(2)从用户处接收查询操作,将底层系统状态以合适的形式呈现给用户。
表现层说明:
典型的用户是人类用户,但是也可能是别的计算机系统。例如如果 ERP 系统要访问我们的系统获取信息,它也是一种用户。
o 不同类型的用户需要不同形式的用户接口,例如为人类用户提供 Web 界面和手机 App,为 ERP 软件用户提供 REST 服务接口。所以,REST 服务接口 也算是表现层。
o 不同类型的用户需要不同形式的数据表示,包括表现形式的不同(XML、JSON、HTML)和内容的不同(例如手机 App 中呈现的数据内容往往比 Web 页面中呈现的少)。
o 表现层对应用层进行封装,表现层的操作与应用层上定义的操作通常是一一对应的关系。表现层从外部用户处接受输入,转换成应用层方法的参数形式,调用应用层方法将任务交由底层系统执行,并将返回结果转换成合适的形式返回给外部用户。
表现层的典型任务是下面三个:
校验——校验外部客户输入的数据是否合法;
转换——将外部客户的输入转换成对底层系统的方法调用参数,以及将底层系统的调用结果转换成外部客户需要的形式;
转发——将外部客户的请求转发给底层系统。
门面层(Faced Layer)
门面层隔离前台和后台系统,定义特定于表现层的数据结构,从后台获取数据内容并转化为表现层的数据形式。
从表现层中分离出专门的门面层,具有下面的优势:
使得表现层能够独立于后台系统,与后台系统并行开发。
表现层通过门面层接口达到和应用层、领域层解耦,意味着表现层可以独立开发,不必等待后台系统的完成,亦不受后台系统重构的影响,在需求调研阶段系统原型出来并得到用户确认之后,就可以开始表现层的开发了。
但是实际上,如果是小系统,往往参数和后台都是一个人做和对接,也无所谓门面不门面的。
使得分布式部署成为可能。
如果没有门面层的隔离,表现层只能直接使用领域层的领域对象作为自己的数据展现结构。
这样我们就不能将系统进行分布式部署,将表现层和后台系统(领域层、应用层等)分别部署到不同的服务器上。
因为在JPA和Hibernate等技术实现中,领域实体绑定到当前服务器的持久化上下文中,必须脱管之后才能够跨越JVM进行传输。
避免Hibernate中“会话已关闭”的问题,消除成本巨大的“Open Session in View”模式的需要。
在采用JPA或Hibernate作为持久化手段的系统中,存在臭名昭著的“会话已关闭”问题,对付这一问题的主要手段是使用Open Session in View方案,但是这个方案的性能很低。
把事务作用范围控制在后端,缩短事务的跨度,提升性能和系统的吞吐量。
更大的问题是事务问题,事务要跨越服务器的边界,复杂性增加,性能严重下降。门面层的存在使得实体和事务都限制在后台系统,不需要扩展到前台服务器。
如果不采用门面层隔离后台数据结构,在前端展现数据需要访问实体的延迟初始化属性时,就会遇到“会话已关闭”问题,而采用Open Session in View模式去解决的话,就意味着事务不是在后端独立完成,这样事务就扩展到前端表现层,在大流量、高吞吐的网站上,把事务扩展到前端界面做造成事务时间跨度极度拉长,从而带来严重的严重的性能问题,大大降低吞吐量。
采用门面模式的话,有关联关系的数据在后台拼装完毕再一次性返回给前端,事务局限在后端范围,不再有“会话已关闭”和性能问题。
门面层说明:
门面层特定于表现层,由表现层定义和控制(包括操作和数据的形式和内容),这意味着需要为不同类型的表现层开发专门的门面层。
查询结果通常以数据传输对象(DTO)的形式表示。DTO的结构由表现层而不是后端决定,代表前端需要的数据形式,与底层数据结构脱耦。
通过门面层实现类访问后端的应用层。实现类将后端数据拼装为DTO并返回给前端,它可以将数据装配职责委托给专门的Assembler工具类去执行。
在分布式系统中,可以在前端和后端分别部署门面层。前后端的门面层接口相同,但后端的门面层实现类负责数据装配和发布,前端的门面层实现类负责通过某种通信机制(Web Service等)与后端门面层通讯,获取后者装配好的数据。传输过程中DTO可能序列化为JSON或XML等形式。
分层架构的优点
分层架构的目的是通过关注点分离来降低系统的复杂度,同时满足单一职责、高内聚、低耦合、提高可复用性和降低维护成本。
单一职责:每一层只负责一个职责,职责边界清晰,如持久层只负责数据查询和存储,领域层只负责处理业务逻辑。
高内聚:分层是把相同的职责放在同一个层中,所有业务逻辑内聚在领域层。
这样做有什么好处呢?试想一下假如业务逻辑分散在每一层,修改功能需要去各层修改,测试业务逻辑需要测试所有层的代码,这样增加了整个软件的复杂度和测试难度。
低耦合:依赖关系非常简单,上层只能依赖于下层,没有循环依赖。
可复用:某项能力可以复用给多个业务流程。
比如持久层提供按照还款状态查询信用卡的服务,既可以给申请信用卡做判断使用,也可以给展示未还款信用卡使用。
易维护:面对变更容易修改。把所有对外接口都放在对外接口层,一旦外部依赖的接口被修改,只需要改这个层的代码即可。
以上这些既是分层的好处也是分层的原则,大家在分层时需要遵循以上原则,不恰当的分层会违背了分层架构的初衷。
分层架构的缺点
开发成本高:因为多层分别承担各自的职责,增加功能需要在多个层增加代码,这样难免会增加开发成本。但是合理的能力抽象可以提高了复用性,又能降低开发成本。
性能略低:业务流需要经过多层代码的处理,性能会有所消耗。
可扩展性低:因为上下层之间存在耦合度,所有有些功能变化可能涉及到多层的修改。
DDD的特点
DDD到底能帮助我们什么呢?
用一段话总结下:DDD改变了数据表驱动的设计方式,设计驱动方向变为Domain Model驱动。
使用DDD的好处
领域模型是整个软件的核心,是软件中最有价值和最具竞争力的部分,设计足够精良且符合业务需求的领域模型能够更快速的响应需求变化。
通用领域模型语言:在有界的上下文中形成统一的语言,方便与产品同学或者开发同学之间沟通,减少歧义和沟通成本。通用领域模型语言:DDD帮助统一语言,在有界的上下文中形成通用的语言,方便与产品同学或者开发同学之间沟通,减少歧义和沟通成本。
业务领域知识沉淀:领域驱动设计的核心是建立统一的领域模型,领域模型不同于数据模型,和任何技术实现及存储实现无关,只反映业务本身,业务通过核心稳定的领域模型,领域知识进行传递,沉淀业务知识。
系统的架构设计:传统的开发设计方式,数据模型驱动是从数据出发,设计数据库表,编写DAO,然后进行业务实现。而领域驱动设计从领域出发,分析领域内模型及其关系,并进行领域建模,设计核心业务逻辑,完成了领域模型与数据模型分离,业务复杂度与技术复杂度分离。
系统的具体实现:领域驱动设计领域建模完成后,确定了业务和应用边界,保证业务模型与代码模型的一致性,进而再进行技术细节实现。领域模型确保了我们的软件的业务逻辑都在一个模型模块中,提高软件的可维护性,业务可理解性以及可重用性。
系统的扩展性:领域模型划分出的边界,沉淀的核心稳定的领域模型知识,面对新来的需求可以快速判断需求的合理性,需求的归属子域,应该在哪个模块实现,通过不断的抽象、不断的分治、拉齐团队内成员对需求的认知,应对系统复杂性,让设计更加清晰和规范。
DDD的难点
DDD有这么多优势,为什么大家使用的还是不够多呢?任何一个事物都有两面性,不可能是完美,DDD也有很多问题,而且有些问题可能就是致命的,让人望而却步的?
但是实际上从Domain Model从哪里来?
实际开发中大多从数据表抽象而来。只是通过基础设施层,我们将对数据表的操作转化为了对领域模型的操作,从而屏蔽了对数据表直接操作的复杂性。但是实际上,面向数据表编程是极其需要的。
大型互联网应用性能往往在数据库层,开发人员如果对数据进行操作时候,不考虑sql怎么写,表结构和关系如何,性能往往会成为后期的事故或者瓶颈。
对于简单的数据操作可以,如果涉及复杂对象之间的互动,对于编排的要求将会非常高,由于隔层不可见,后期维护层本将会非常高
实际上业务人员并不会写代码,只能传递需求
要求、难度系数高:领域模型的正确构建首先需要有一个熟悉业务、建模的领域专家,其次依赖编程人员对DDD的深刻理解,对团队成员的本身素质要求较高。
效率,投入产出比:正确的建模从方案讨论、设计、实践、落地往往需要花费一段时间,面对业务紧急的需求以及倒排的工期,可能满足不了上线的要求。而其他一般架构不需要这些时间,短期投入成本高,但是从长期看,领域模型收益还是很高的。
团队成员之间协作:领域模型一般是整体的,领域内是相互依赖的,相互影响的,不容易分割为可并行独立解耦开发的模块,对开发同学的协作要求较高。
技术上的缺陷:DDD是基于聚合来组织代码,对于高性能场景下,加载聚合中大量的无用字段会严重影响性能,实际场景中,更多的高流量查询往往脱离聚合直接对某一个数据进行查询。此外,事务被限定在限界上下文中,跨多个限界上下文的场景需要开发者额外考虑分布式事务问题。
能搞定DDD编程的,必定不会是互联网,因为ddd需要长期投入优化,可能很长一段时间都没有商业价值和可实施人员。
领域驱动设计一般分为两个阶段
领域驱动设计划分了战略设计和战术设计,也提供了诸多模式和工具,但却没有一个统一过程去规范这两个阶段需要执行的活动、交付的工件以及阶段里程碑,甚至没有清晰定义这两个阶段如何衔接、它们之间执行的工作流到底是怎么样的。
除了把领域驱动设计 分为战略设计和战术设计的方法之外,《解构-领域驱动设计》提出的 DDDRUP 方法。
战略设计侧重于高层次、宏观上去划分和集成限界上下文,而战术设计则关注更具体使用建模工具来细化上下文。
(1)战略阶段。
在战略设计中,讲求的是子域和限界上下文的划分,以及各个限界上下文之间的上下游关系,更偏向于软件架构,帮助我们从一个宏观的角度观察和审视软件系统。
分解问题域:
通常先进行需求调研,收集领域知识,用例设计,
引入限界上下文(Bounded Context)和上下文映射(Context Map)对问题域进行合理的分解,
识别出核心领域(Core Domain)与子领域(SubDomain)
并确定领域的边界以及它们之间的关系,维持模型的完整性。
架构方面:
通过分层架构来隔离关注点,尤其是将领域实现独立出来,能够更利于领域模型的单一性与稳定性;
引入六边形架构可以清晰地表达领域与技术基础设施的边界;
CQRS 模式则分离了查询场景和命令场景,针对不同场景选择使用同步或异步操作,来提高架构的低延迟性与高并发能力。
在对传统MVC架构系统进行改造时,一般分为接口层、应用层、领域层、基础设施层。
(2)战术阶段。
在战术层面,它主要应对的是领域的复杂性。
领域驱动设计用以表示模型的主要要素包括如下:
实体(Entity)。一个由它的标识定义的对象叫做实体。通常实体具备唯一id,能够被持久化,具有业务逻辑,对应现实世界业务对象。
值对象(Value Object)。描述事物的对象;更准确的说,一个没有标识符描述一个领域方面的对象。
聚合及聚合根(aggregate、aggregate root)。聚合是用来定义领域对象所有权和边界的领域模式。聚合定义了一组具有内聚关系的相关对象的集合,每个聚合都有一个根对象(聚合根实体)。
领域事件(Domain Event)。领域事件的触发点在领域模型中。它的作用避免让领域对象对repository或service设施产生直接依赖。
资源库(Repository)。仓储(资源库)是用来管理实体的集合。。
工厂(Factory)。当创建实体和值对象复杂时建议使用工厂模式。
服务(services)。服务提供的操作是它提供给使用它的客户端,并突出领域对象的关系。其细分为领域服务和应用服务。
领域服务(Domain Service)。领域服务封装了一些域概念,这些概念并不是自然建模的。领域服务是无状态的,须以非常干净简洁的代码实现。
应用服务(Application Service)。应用程序服务构成应用程序或服务层。
领域驱动设计流程
通常战略阶段和战术阶段要求领域专家和开发团队紧密配合,沟通协调完成,示意图如下:
领域驱动的流程特点
领域驱动指的是以领域作为解决问题切入点,
面对业务需求,先提炼出领域概念,并构建领域模型来表达业务问题,
而构建过程中我们应该尽可能避免牵扯技术方案或技术细节。
而编码实现更像是对领域模型的代码翻译,代码(变量名、方法名、类名等)中要求能够表达领域概念,让人见码明义。
特点之一:思维模式转变
实践 DDD 以前,我最常使用的是数据驱动设计。
它的核心思路针对业务需求进行数据建模:根据业务需求提炼出类,然后通过 ORM 把类映射为表结构,并根据读写性能要求使用范式优化表与表之间的关联关系。
数据驱动是从技术的维度解决业务问题,得出的数据模型是对业务需求的直接翻译,并没有蕴含稳定的领域知识/规则。
一旦需求发生变化,数据模型就得发生变化,对应的库表的设计也需要进行调整。
这种设计思维导致变化从需求穿透到了数据层,中间并没有稳定的,不易变的层级进行阻隔,最终导致系统响应变化的能力很差。
特点之二:协同方式转变
过去由产品同学提出业务需求,研发同学根据业务需求的 产品原型 进行技术方案设计,并编程实现。
这种协同方式的弊端在于:无法形成能够消除认知差异的模型。
产品同学从业务角度提出用户需求,这些需求可能是易变的、定制化的,
而研发同学在缺少行业经验的情况下,往往会选择直译,即根据需求直接转换为数据模型。
而研发同学从技术实现角度设计技术方案,其中涉及很多的技术细节,产品同学无法从中判断是否与自己提出的业务诉求和产品规划相一致,最终形成认知差异。
且认知差异会随着迭代不断被放大,最后系统变成一个大泥球。
DDD 通过解锁新角色**”领域专家"以及模型驱动设计**,有效地降低产品和研发的认知差异。 领域专家是具有丰富行业经验和领域知识储备的人,他们能够在易变的、定制化的需求中提炼出清晰的边界,稳定的、可复用的领域概念和业务规则,并携手产品和研发共同构建出领域模型。
领域模型是对业务需求的知识表达形式,它不涉及具体的技术细节(但能够指导研发同学进行编程实现),因此消除了产品和研发在需求认知上的鸿沟。
而模型驱动设计则要求领域模型能够关联业务需求和编码实现,模型的变更意味着需求变更和代码变更,协作围绕模型为中心。
特点之三:精炼循环
精炼循环指的是在统一语言,提炼领域概念,明确边界,构建模型,绑定实现过程中,这些环节相互影响和反馈,在不断的迭代试错-调整以最终沉淀出稳定的、深层次的模型的过程。
比如,我们在提炼领域概念的时候会觉得统一语言定义不合理/有歧义,此时我们就会调整统一语言的定义,并重新进行提炼领域概念。
通过精炼循环,我们逐步形成稳定的领域模型。在 DDD 中,让领域专家来主导概念提炼、边界划分等宏观设计,原因就在于领域专家的经验和行业洞见来源于过去已经迭代的无数个精炼循环,因此由这些宏观设计推导出来的领域模型,往往都是非常稳定的。
精炼循环的核心是循环,它避免知识只朝单一方向流动,最终因各环节上的认知差异,最终导致模型无法在产品、领域专家和研发中达成一致、模型与实现割裂。
阶段一:战略阶段
战略阶段的工作:引入限界上下文(Bounded Context)和上下文映射(Context Map)对问题域进行合理的分解。
战略阶段的参与人员:产品、领域专家和开发同学。这些人组成 DDD 战略设计会。
领域专家是谁?懂得该领域业务知识的人,领域专家一般不是技术人员,通常技术人员对新软件项目的领域知识是不了解的。
领域专家通常是软件设计者或者产品经理,也可能是销售、技术支持等收集业务需求的人。
技术人员与领域专家之间需要使用领域通用语言(Ubiquitous Language)进行业务沟通,领域通用语言可以是 UML、需求文档等提前规定好的双方都能理解的语言。
在战略设计中,讲求的是子域和限界上下文的划分,以及各个限界上下文之间的上下游关系,更偏向于软件架构,帮助我们从一个宏观的角度观察和审视软件系统。
领域内所有限界上下文的领域模型构成整个领域的领域模型。
DDD 战略设计会建立领域模型,领域模型可以用于指导微服务的设计和拆分。
领域通用语言和限界上下文(Bounded Context)
通用语言:就是能够简单、清晰、准确描述业务涵义和规则的语言。
限界上下文:用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。
下面这张图描述了从事件风暴建立通用语言到领域对象设计和代码落地的完整过程。
设计过程中我们可以用一些表格,来记录事件风暴和微服务设计过程中产生的领域对象及其属性。
比如,领域对象在 DDD 分层架构中的位置、属性、依赖关系以及与代码对象的映射关系等。
下面是一个微服务设计实例的部分数据,表格中的这些名词术语就是项目团队在事件风暴过程中达成一致、可用于团队内部交流的通用语言。
在这个表格里面我们可以看到,DDD 分析过程中所有的领域对象以及它们的属性都被记录下来了,除了 DDD 的领域对象,我们还记录了在微服务设计过程中领域对象所对应的代码对象,并将它们一一映射。
限界上下文 (Bounded Context)
通用语言也有它的上下文环境,为了避免同样的概念或语义在不同的上下文环境中产生歧义,DDD 在战略设计上提出了“限界上下文”这个概念,用来确定语义所在的领域边界。
限界上下文是一个显式的语义和语境上的边界,领域模型便存在于边界之内。
限界上下文包含两部分:限界(Bounded)和上下文(Context)。限界就是领域的边界, 而上下文就是语义环境, 通过限界上下文让所有交流的人讨论的范围在同一个领域边界内。
边界内,通用语言中的所有术语和词组都有特定的含义。
把限界上下文拆解开看,限界就是领域的边界,而上下文则是语义环境。
通过领域的限界上下文,我们就可以在统一的领域边界内用统一的语言进行交流。
总之,限界上下文用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等概念有一个确切的含义。
领域的核心思想就是分治思想:就是将问题域逐级细分,来降低业务理解和系统实现的复杂度。
三种Context集成方式
RPC 方式
消息队列或者发布 - 订阅机制
RESTful 方式
上下文映射 (依赖)的种类
合作关系
合作关系存在于两个团队之间。
每个团队各自负责一个限界上下文。两个团队通过互相依赖的一整套目标联合起来形成合作关系。一损俱损,一荣俱荣。
由于相互之间的联系非常紧密,他们经常会对同步日程安排和相互关联的工作。他们还必须使用持续集成对保持集成工作协调一致。
共享内核
两个或者多个团队之间共享着一个小规模但却通用的模型。
团队必须就要共享的模型元素达成一致。有可能他们当中只有一个团队会维护,构建及测试共享模型的代码。
客户 - 供应商
两个限界上下文中,一方是供应商处于上游,一方是客户方处于下游。支配这种关系的是供应商,因为它必须提供客户需要的东西。客户需要与供应商共同制订规划来满足各种预期,但最终却还是由供应商来决定客户获得的是什么以及何时获得。
跟随者
上游团队没有任何动机去满足下游团队的具体需求。由于各种原因,下游团队也无法投入资源去翻译上游模型的通用语言来适应自己的特定需求,因此只能顺应上游的模型。例如当一个团队需要与一个非常庞大复杂的模型集成,而且这个模型已经非常成熟时,团队往往会成为它的跟随者。
防腐层 ACL
这是最具防御性的上下文映射关系,下游团队在其通用语言(模型)和位于它上游的通用语言(模型)之间创建了一个翻译层。
防腐层隔离了下游模型与上游模型,并完成了两者之间的翻译。
引入防腐层的目的是为了隔离耦合。防腐层往往位于下游,通过它隔离上游上下文发生的变化。
所以,这也是一种集成方式。
对于进程内的开放主机服务,称为本地服务(对应 DDD 中的应用服务)。
对于进程间的开放主机服务,成为远程服务。根据选择的分布式通信技术的不同,又可以定义出类型不同的远程服务:
面向服务行为,比如基于 RPC,称为提供者(Provider);
面向服务资源,比如基于 REST,称为资源(Resource);
面向事件,比如基于消息中间件,称为订阅者(Subscriber);
面向视图模型,比如基于 MVC,称为控制器(Controller);
该协议是开放的,所有需要与限界上下文进行集成的客户端都可以相对轻松地使用它。
通过应用程序编程接口提供的服务都有详细的文档,用起来也很舒服。
u表示 upstream 上游,d 表示down stream下游。
阶段二:战术阶段
战术设计也称为战术建模,从技术视角出发,以领域模型为输入,通过限界上下文作为服务划分的边界进行微服务拆分,
在每个微服务中进行领域分层,实现领域服务,从而实现领域模型对于代码映射目的,最终实现DDD的落地实。
包括:实体、值对象、聚合、聚合根、资源库、工厂、领域服务、领域事件、应用服务等代码逻辑的设计和实现。
实体和值对象
在 DDD 中,实体和值对象是很基础的领域对象。
实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。
人员实体原本包括:姓名、年龄、性别以及人员所在的省、市、县和街道等属性。
这样显示地址相关的属性就很零碎了对不对?
现在,我们可以将 “省、市、县和街道等属性” 拿出来构成一个“地址属性集合”,这个集合就是值对象了。
领域事件总体架构
领域事件的执行需要一系列的组件和技术来支撑。
我们来看一下这个领域事件总体技术架构图,领域事件处理包括:事件构建和发布、事件数据持久化、事件总线、消息中间件、事件接收和处理等。
领域服务(Domain Service)和应用服务(Application Service)
战略层语境:
领域服务通常指相对聚焦的底层支撑域/通用域服务
应用服务通常指面向业务场景负责功能组装的服务
战术层语境:
领域服务指领域建模工具集中所指的“领域服务”, 处于领域层
应用服务指面向场景的技术实现组装,处于应用层
分层架构
在《实现领域驱动设计》一书中,DDD 分层架构有一个重要的原则:每层只能与位于其下方的层发生耦合。分层架构可以简单分为两种,即严格分层架构和松散分层架构。
在严格分层架构中,某层只能与位于其直接下方的层发生耦合,而在松散分层架构中,则允许某层与它的任意下方层发生耦合
关于分层架构的优点,Martin Fowler在《Patterns of Enterprise Application Architecture》一书中给出了答案:
开发人员可以只关注整个结构中的某一层。
可以很容易的用新的实现来替换原有层次的实现。
可以降低层与层之间的依赖。
有利于标准化。
利于各层逻辑的复用。
“金无足赤,人无完人”,分层架构也不可避免具有一些缺陷:
降低了系统的性能。这是显然的,因为增加了中间层,不过可以通过缓存机制来改善。
可能会导致级联的修改。这种修改尤其体现在自上而下的方向,不过可以通过依赖倒置来改善。
六边形架构
六边形架构是 Alistair Cockburn 在2005年提出,解决了传统的分层架构所带来的问题,实际上它也是一种分层架构,只不过不是上下或左右,而是变成了内部和外部。
六边形架构又名“端口-适配器架构”:
CQRS架构
CQRS架构-命令查询职责分离
CQRS本身只是一个读写分离的架构思想,全称是:Command Query Responsibility Segregation,即命令查询职责分离,表示在架构层面,将一个系统分为写入(命令)和查询两部分:
一个命令(如写入)表示一种意图,表示命令系统做什么修改,命令的执行结果通常不需要返回;
一个查询表示向系统查询数据并返回。
CQRS架构中,另外一个重要的概念就是事件,
事件表示命令操作领域中的聚合根,然后聚合根的状态发生变化后产生的事件。
洋葱架构
领域驱动落地框架
1 COLA框架
cola框架是阿里大佬张建飞(Frank) 基于DDD构建的平台应用框架。
“让COLA真正成为应用架构的最佳实践,帮助广大的业务技术同学,脱离酱缸代码的泥潭!”
csdn地址:https://blog.csdn.net/significantfrank/article/details/110934799
2 leave-sample
中台架构与实现 DDD和微服务,清晰地提供了从战略设计到战术设计以及代码落地。
leave-sample地址:https://gitee.com/serpmelon/leave-sample
3 dddbook
阿里技术专家详解DDD系列,例子精炼,项目代码结构与rdfa相似,极具参考价值。
dddbook地址:https://developer.aliyun.com/article/719251
4 Xtoon
xtoon-boot是基于领域驱动设计(DDD)并支持SaaS平台的单体应用开发脚手架。
重点研究如何应用。xtoon-boot提供了完整落地方案和企业级手脚架;
gitee地址:https://gitee.com/xtoon/xtoon-boot
github地址:https://github.com/xtoon/xtoon-boot
5 DDD Lite
DDD 领域驱动设计微服务简化版,简洁、高效、值得重点研究。
gitee地址:https://gitee.com/litao851025/geekhalo-ddd
快速入门:https://segmentfault.com/a/1190000018464713
快速构建新闻系统:https://segmentfault.com/a/1190000018254111
6 ruoyi_cloud
若依快速开发平台,以该项目建立对阳光智采和rdfa的技术框架基准线。
gitee地址:https://gitee.com/y_project/RuoYi-Cloud
7 Axon Framework
Axon Framework 是用来帮助开发人员构建基于命令查询责任分类(Command Query Responsibility Segregation: CQRS)设计模式的可伸缩、可扩展和可维护应用程序的框架。你只需要把工作重心放在业务逻辑的设计上。通过一些 Annotation ,Axon 使得你的代码和测试分离。
https://www.oschina.net/p/axon
https://www.jianshu.com/p/15484ed1fbde
DDD 与微服务的关系
微服务拆解指的是把一个单体服务拆分为粒度“足够小”的多个服务,而这里的“足够小”是一个主观的,没有任何标准的定义。
尽管如此,我们对“微”这个词还是有一些基本要求的:足够内聚,足够独立,足够完备,这才使得拆分出来的微服务收益大于投入,试想如果一个微服务提供的业务功能会牵扯到与其他众多微服务的协作,那岂不是芭比 Q 了。
而上述我们对微服务的基本要求,实际上与限界上下文的特征(最小完备,自我履行,稳定空间,独立进化)不谋而合,因此,我们可以把限界上下文映射为微服务。
最后浓缩一下:DDD的四重边界
根据下图所示,我们通过四重边界来进行架构设计:
分而治之:DDD通过规划四重边界,把领域知识做了合理的固化和分层。
业务有核心领域和支持域、业务域中又拆分成多个限界上下文(BC),一个BC中又根据领域知识核心与否进行分层,领域层中按照多个业务(子域)的强相关性进行聚合成一个子域
【第一重边界】确定项目的愿景与目标,确定问题空间,确定核心子领域、通用子领域(多个子领域可以复用)、支撑子领域(额外功能,如数据统计、导出报表)
【第二重边界】解决方案空间里的限界上下文,就是一道进程隔离层面的物理边界
【第三重边界】每个限界上下文内,使用分层架构划分为:接口层、领域层、应用层、基础设施层之间的最小隔离
【第四重边界】领域层里为了保证各个领域的完整性和一致性,引入聚合的设计作为隔离领域模型的最小单元
附录:什么是POCO和DTO、值对象(Value Object)?
DTO就是数据传输对象(Data Transfer Object),
POCO就是简单CLR对象(Plain Old CLR Object)这个是C#中的概念,公共语言运行时 (CLR),概念来源于Java中的POJO;
值对象(Value Object)是领域驱动设计(Domain-Driven Design,DDD)中的概念,是一个包含数据+逻辑的完整的领域模型。
POCO是DTO和值对象的超集
DTO不能等同于值对象
DTO和值对象都不能有标识Id,而POCO可以有标识Id
实体对象 是DDD里边的概念,可以基于标识ID进行区分,也属于 POCO