可落地的DDD代码实践
前言
网上那么多DDD的文章,但代码工程却没有一个比较好的例子,本文将手把手跟你一起写DDD代码,学习DDD思想与代码相结合带来的好处。
一、从六边形架构谈起
六边形架构也称端口与适配器。对于每种外界类型,都有一个适配器与之相对应。外界通过应用层API与内部进行交互。
六边形架构提倡用一种新的视角来看待整个系统。该架构中存在两个区域,分别是“外部区域”和“内部区域”。在外部区域中,不同的客户均可以提交输入;而内部的系统则用于获取持久化数据,并对程序输出进行存储(比如数据库),或者在中途将输出转发到另外的地方(比如消息,转发到消息队列)。
每种类型的客户都有自己的适配器,该适配器用于将客户输入转化为程序内部API所理解的输入。六边形每条不同的边代表了不同种类型的端口,端口要么处理输入,要么处理输出。图中有3个客户请求均抵达相同的输入端口(适配器A、B和C),另一个客户请求使用了适配器D。可能前3个请求使用了HTTP协议(浏览器、REST和SOAP等),而后一个请求使用了MSMQ的协议。端口并没有明确的定义,它是一个非常灵活的概念。无论采用哪种方式对端口进行划分,当客户请求到达时,都应该有相应的适配器对输入进行转化,然后适配器将调用应用程序的某个操作或者向应用程序发送一个事件,控制权由此交给内部区域。
二、依赖倒置
依赖倒置的原则(DIP)由Robert C. Martin提出,核心的定义是:
高层模块不应该依赖于底层模块,两者都应该依赖于抽象
抽象不应该依赖于实现细节,实现细节应该依赖于接口
按照DIP的原则,领域层就可以不再依赖于基础设施层,基础设施层通过注入持久化的实现就完成了对领域层的解耦,采用依赖注入原则的新分层架构模型就变成如下所示:
采用了依赖注入方式后,其实可以发现事实上已经没有分层概念了。无论高层还是底层,实际只依赖于抽象,整个分层好像被推平了。
三、DDD 代码分层
整体代码结构
- com.${company}.${system}.${appname}
|- ui(用户接口层)
|service
|- impl
|- web
|- controller
|- filter
|- application(应用层)
|- service
|- impl
|- command
|- query
|- dto
|- mq
|- domain(领域层)
|- service
|- facade
|- model
|- event
|- repository
|- infrastructure(基础设施层)
|- dal
|-dos
|-dao
|- mapper
|- factory
3.1 用户接口层
用户接口层作为对外的门户,将网络协议与业务逻辑解耦。可以包含鉴权、Session管理、限流、异常处理、日志等功能。
返回值一般使用{"code":0,"msg":"success","data":{}}
的格式进行返回。
一般会封装一些公共的Response
对象,参考如下:
public class Response implements Serializable {
private boolean success;
private String code;
private String msg;
private Map<String, Object> errData;
}
public class SingleResponse<T> extends Response {
private T data;
}
public class ListResponse<T> extends Response {
private int count = 0;
private int pageSize = 20;
private int pageNo = 1;
private List<T> data;
}
用户接口层的接口,无需与应用接口保持一一对应,应该保证不同的场景使用不同的接口,保证后续业务的兼容性与可维护性。
3.2 应用层
应用层连接用户接口层和领域层,主要协调领域层,面向用例和业务流程,协调多个聚合完成服务的组合和编排,在这一层不实现任何业务逻辑,只是很薄的一层。
应用层的核心类:
- ApplicationService应用服务:最核心的类,负责业务流程的编排,但本身不负责任何业务逻辑。有时会简写为“AppService”。一个ApplicationService类是一个完整的业务流程,其中每个方法负责处理一个Use Case,比如订单的各种用例(下单、支付成功、发货、收货、查询)。
- DTO Assembler:负责将内部领域模型转化为可对外的DTO。
- 返回的DTO:作为ApplicationService的出参。
- Command指令:指调用方明确想让系统操作的指令,其预期是对一个系统有影响,也就是写操作。通常来讲指令需要有一个明确的返回值(如同步的操作结果,或异步的指令已经被接受)。
- Query查询:指调用方明确想查询的东西,包括查询参数、过滤、分页等条件,其预期是对一个系统的数据完全不影响的,也就是只读操作。
- Event事件:指一件已经发生过的既有事实,需要系统根据这个事实作出改变或者响应的,通常事件处理都会有一定的写操作。事件处理器不会有返回值。这里需要注意一下的是,Application层的Event概念和Domain层的DomainEvent是类似的概念,但不一定是同一回事,这里的Event更多是外部一种通知机制而已。
ApplicationService的接口入参只能是一个Command、Query或Event对象,CQE对象需要能代表当前方法的语意。这样的好处是提升了接口的稳定性、降低低级的重复,并且让接口入参更加语意化。
案例代码
public interface UserAppService {
UserDTO add(@Valid AddUserCommand cmd);
List<UserDTO> query(UserQuery query);
}
@Data
public class AddUserCommand {
private Integer age;
private String name;
...
}
@Data
public class OrderQuery {
private Long userId;
private int pageNo;
private int pageSize;
}
@Data
public class UserDTO {
private Long userId;
private Integer age;
private String name;
...
}
针对于不同语意的指令,要避免CQE对象的复用。反例:一个常见的场景是“Create创建”和“Update更新”,一般来说这两种类型的对象唯一的区别是一个ID,创建没有ID,而更新则有。所以经常能看见有的同学用同一个对象来作为两个方法的入参,唯一区别是ID是否赋值。这个是错误的用法,因为这两个操作的语意完全不一样,他们的校验条件可能也完全不一样,所以不应该复用同一个对象。正确的做法是产出两个对象。
3.2 1 Response vs Exception
Interface层的HTTP和RPC接口,返回值为Result,捕捉所有异常。
Application层的所有接口返回值为DTO,不负责处理异常。
3.2.2 CQE vs DTO
表面上看,两种对象都是简单的POJO对象,但其实是有很大区别的:
- CQE: 是ApplicationService的输入,有明确的“意图”,对象的内部需要保证其正确性。每一个CQE都是有明确“意图”的,所以要尽量避免CQE的复用,哪怕所有参数都一样,只要语义不同,就不应该复用。
- DTO: 只是数据容器,只是为了和外部交互,所以本身不包含任何逻辑,只是贫血对象。
因为CQE是有“意图”的,所以,理论上CQE的数量是无限的。但DTO作为数据容器,是和模型对应的,所以是有限的。
3.2.3 Anti-Corruption Layer防腐层
在ApplicationService中,经常会依赖外部服务,从代码层面对外部系统产生了依赖。比如创建一个用户时,可能依赖了帐号服务,这个时候我们引入防腐层。防腐层的类名一般用“Facade”。
ACL防腐层的实现方式:
- 对于依赖的外部对象,我们抽取出所需要的字段,生成一个内部所需的VO或DTO类。
- 构建一个新的Facade,在Facade中封装调用链路,将外部类转化为内部类。
- 针对外部系统调用,同样的用Facade方法封装外部调用链路。
3.3 领域层
领域层是领域模型的核心,主要实现领域模型的核心业务逻辑,体现领域模型的业务能力。领域层关注实现领域对象的充血模型和聚合本身的原子业务逻辑,至于用户操作和业务流程,则交给应用层去编排。这样设计可以保证领域模型不容易受外部需求变化的影响,保证领域模型的稳定。
领域层的核心类:
- 实体类(Entity):大多数DDD架构的核心都是实体类,实体类包含了一个领域里的状态、以及对状态的直接操作。Entity最重要的设计原则是保证实体的不变性(Invariants),也就是说要确保无论外部怎么操作,一个实体内部的属性都不能出现相互冲突,状态不一致的情况。
- 值对象(VO):通常是用来度量和描述事物。我们可以非常容易的对其进行创建,测试,使用,优化和维护,所以在建模时,我们尽量采用值对象来建模。
- 聚合根(Aggr):聚合是由业务和逻辑紧密关联的实体和值对象组合而成的。聚合是数据修改和持久化的基本单元。每个聚合都有一个根实体,叫做聚合根,外界只能通过聚合根跟聚合通信。聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致的问题。
- 领域服务(DomainService):当某个操作不适合放在聚合和值对象上时,最好的方式便是使用领域服务了。可以使用领域服务的地方,过度使用领域服务将导致贫血领域模型。执行一个显著的业务操作过程;对领域对象进行转换;已多个领域对象作为输入进行计算,结果产生一个值对象。
- 仓储层接口(Repository):把我们要的数据当做一个集合放在仓储里面,想要的时候直接获取。仓储作为领域层和基础结构层的连接组件,使得领域层不必过多的关注存储细节。在设计时,将仓储接口放在领域层,而将仓储的具体实现放在基础结构层,领域层通过接口访问数据存储,而不必过多的关注仓储存储数据的细节,这样使得领域层将更多的关注点放在领域逻辑上面。
- 工厂(Factory):对于实体等对象构造比较麻烦的,可以借助工厂进行构造。
通常,对于实体、值对象、聚合根,我们不可以不加类后缀,这样更能体现领域对象本身的含义。
public class Order {
// OrderId是隐性的概念显性化,而不是直接使用一个String,String就只能表示一个值了
private OrderId orderId;
private BuyerId buyerId;
private OrderStatus status;
private Long amount;
private List<OrderItem> orderItems;
public static Order create(...) {
// 如果参数比较多,构造比较麻烦,可以迁移到 Factory
...
}
public void pay(...) {
}
public void deliver(...) {
}
...
}
public class OrderItem {
private Long goodsId;
private Integer count;
public static OrderItem create(Long goodsId, Integer count) {
...
}
}
// 领域服务一般无需接口定义
public class OrderDomainService {
@Resource
private OrderRepository orderRepository;
public Order create(Order order) {
...
orderRepository.create(order);
return order;
}
}
public interface OrderRepository {
void add(Order order);
Order getByOrderId(OrderId orderId);
}
3.4 基础设施层
主要负责技术细节处理,比如数据库CRUD、缓存、消息服务等。
public class OrderDO {
}
public class OrderItemDO {
}
public class OrderDao implements OrderRepository {
@Resource
private OrderMapper orderMapper;
@Resource
private OrderItemMapper orderItemMapper;
@Override
public void add(Order order) {
OrderDO orderDO = OrderFactory.build(order);
List<OrderItemDO> orderItemDOList = OrderFactory.build(order);
orderMapper.insert(orderDO);
orderItemMapper.batchInsert(orderItemDOList);
}
}
参考资料
- 《实现领域驱动设计》
- 殷浩详解DDD:如何避免写流水账代码
- 殷浩详解DDD:领域层设计规范
可落地的DDD代码实践的更多相关文章
- 可落地的DDD(5)-战术设计
摘要 本篇是DDD的战术篇,也就是关于领域事件.领域对象.聚合根.实体.值对象的讨论.也是DDD系列的完结篇. 这一部分在我们团队争论最多的,也有很多月经贴,比如对资源库的操作应该放在领域服务还是领域 ...
- 可落地的DDD(4)-如何利用DDD进行微服务的划分(2)
摘要 在前面一篇介绍了如何通过DDD的思想,来调整单体服务内的工程结构,为微服务的拆分做准备.同时介绍了我们在进行微服务拆分的时候踩过的一些坑. 这篇介绍下我们最终的方案,不一定对,欢迎留言讨论. 微 ...
- 如丝般顺滑:DDD再实践之类目树管理
在上次反思DDD实践之后,在类目树管理项目中再次实践DDD.从需求分析到建模和具体的落地,结合个人体会,都是干货.
- 可落地的DDD(7)-战术设计上的一些误区
背景 几年前我总结过DDD战术设计的一些落地经验可落地的DDD(5)-战术设计,和一次关于聚合根的激烈讨论最近两年有些新的落地体验,回过头来发现,当初对这些概念的理解还是没有深入,这篇文章重新阐述下. ...
- 可落地的DDD(3)-如何利用DDD进行微服务的划分
摘要 前面两篇介绍了DDD的目标管理.DDD的工程结构调整.这篇讨论微服务的划分.微服务是目前后端比较流行的架构体系了,那么如何做好一个微服务的划分?一个微服务的粒度应该是多大呢?这篇主要介绍如何结合 ...
- ReactiveCocoa代码实践之-更多思考
三.ReactiveCocoa代码实践之-更多思考 1. RACObserve()宏形参写法的区别 之前写代码考虑过 RACObserve(self.timeLabel , text) 和 RACOb ...
- ReactiveCocoa代码实践之-RAC网络请求重构
前言 RAC相比以往的开发模式主要有以下优点:提供了统一的消息传递机制:提供了多种奇妙且高效的信号操作方法:配合MVVM设计模式和RAC宏绑定减少多端依赖. RAC的理论知识非常深厚,包含有FRP,高 ...
- 深刻理解Python中的元类(metaclass)--代码实践
根据http://blog.jobbole.com/21351/所作的代码实践. 这篇讲得不错,但以我现在的水平,用到的机会是很少的啦... #coding=utf-8 class ObjectCre ...
- Java的BIO和NIO很难懂?用代码实践给你看,再不懂我转行!
本文原题“从实践角度重新理解BIO和NIO”,原文由Object分享,为了更好的内容表现力,收录时有改动. 1.引言 这段时间自己在看一些Java中BIO和NIO之类的东西,也看了很多博客,发现各种关 ...
随机推荐
- Python基础(sorted)
arr1 = [1,2,3,-30,4,5,-6] arr2 = sorted(arr1)#sorted()函数就可以对list进行排序 arr3 = sorted(arr1,key=abs)#可以接 ...
- 使用Token进行CSRF漏洞防御
1.登录验证成功之后,在会话SESSION["user_token"]中保存Token. 2.在后台操作中,增删改表单中添加隐藏域hidden,设置value为Token. 3.提 ...
- m3u8 ts 视频流爬取思路,合成
.... 先开调试,输入查找一下有没有 m3u8 文件 然后下下来用Notepad++ 打开一下 (以下的样子) 这里就是整个视频的视频流, .ts 的都是文件,都下下来, ------------ ...
- 菜鸡的Java笔记 第八 - java 面向对象
面向对象的特点以及开发过程. java中最大的特点是其支持面向对象编程设计思想.在面向对象之前广泛流传的是面向过程的编程思想,例如:C语言的开发就属于面向过程 如果要想更简单的去理解面向过 ...
- java 必应壁纸批量下载
基于java 必应壁纸批量下载 - rookie丶k - 博客园 (cnblogs.com)实现 上面代码运行本地有点小问题,改了改 1.ssl验证 2.请求头 3.需要优化下载速度,多线程方式(还不 ...
- SpringCloud升级之路2020.0.x版-42.SpringCloudGateway 现有的可供分析的请求日志以及缺陷
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 网关由于是所有外部用户请求的入口,记录这些请求中我们需要的元素,对于线上监控以及业务问题定 ...
- myeclipse字体大小格式的设置
- 如何使用scp在Linux服务器的后台传输文件?
目录 一.上传 常规操作 建议 后台运行 二.下载 两台服务器间文件如何传输?对于小文件,可以先从Linux服务器传到window,再传到另一台服务器.对于大的文件,如测序数据.比对文件等.这样的方法 ...
- 【GS模型】全基因组选择之rrBLUP
目录 1. 理论 2. 实操 2.1 rrBLUP包简介 2.2 实操 3. 补充说明 关于模型 关于交叉验证 参考资料 1. 理论 rrBLUP是基因组选择最常用的模型之一,也是间接法模型的代表.回 ...
- MYSQL5.8-----4
cc