网上那么多DDD的文章,但代码工程却没有一个比较好的例子,本文将手把手跟你一起写DDD代码,学习DDD思想与代码相结合带来的好处。
六边形架构也称端口与适配器。对于每种外界类型,都有一个适配器与之相对应。外界通过应用层API与内部进行交互。
六边形架构提倡用一种新的视角来看待整个系统。该架构中存在两个区域,分别是“外部区域”和“内部区域”。在外部区域中,不同的客户均可以提交输入;而内部的系统则用于获取持久化数据,并对程序输出进行存储(比如数据库),或者在中途将输出转发到另外的地方(比如消息,转发到消息队列)。
每种类型的客户都有自己的适配器,该适配器用于将客户输入转化为程序内部API所理解的输入。六边形每条不同的边代表了不同种类型的端口,端口要么处理输入,要么处理输出。图中有3个客户请求均抵达相同的输入端口(适配器A、B和C),另一个客户请求使用了适配器D。可能前3个请求使用了HTTP协议(浏览器、REST和SOAP等),而后一个请求使用了MSMQ的协议。端口并没有明确的定义,它是一个非常灵活的概念。无论采用哪种方式对端口进行划分,当客户请求到达时,都应该有相应的适配器对输入进行转化,然后适配器将调用应用程序的某个操作或者向应用程序发送一个事件,控制权由此交给内部区域。
依赖倒置的原则(DIP)由Robert C. Martin提出,核心的定义是:
高层模块不应该依赖于底层模块,两者都应该依赖于抽象 抽象不应该依赖于实现细节,实现细节应该依赖于接口
按照DIP的原则,领域层就可以不再依赖于基础设施层,基础设施层通过注入持久化的实现就完成了对领域层的解耦,采用依赖注入原则的新分层架构模型就变成如下所示:
采用了依赖注入方式后,其实可以发现事实上已经没有分层概念了。无论高层还是底层,实际只依赖于抽象,整个分层好像被推平了。
整体代码结构
- 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
用户接口层作为对外的门户,将网络协议与业务逻辑解耦。可以包含鉴权、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; }
用户接口层的接口,无需与应用接口保持一一对应,应该保证不同的场景使用不同的接口,保证后续业务的兼容性与可维护性。
应用层连接用户接口层和领域层,主要协调领域层,面向用例和业务流程,协调多个聚合完成服务的组合和编排,在这一层不实现任何业务逻辑,只是很薄的一层。
应用层的核心类:
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是否赋值。这个是错误的用法,因为这两个操作的语意完全不一样,他们的校验条件可能也完全不一样,所以不应该复用同一个对象。正确的做法是产出两个对象。
Interface层的HTTP和RPC接口,返回值为Result,捕捉所有异常。
Application层的所有接口返回值为DTO,不负责处理异常。
表面上看,两种对象都是简单的POJO对象,但其实是有很大区别的:
因为CQE是有“意图”的,所以,理论上CQE的数量是无限的。但DTO作为数据容器,是和模型对应的,所以是有限的。
在ApplicationService中,经常会依赖外部服务,从代码层面对外部系统产生了依赖。比如创建一个用户时,可能依赖了帐号服务,这个时候我们引入防腐层。防腐层的类名一般用“Facade”。
ACL防腐层的实现方式:
领域层是领域模型的核心,主要实现领域模型的核心业务逻辑,体现领域模型的业务能力。领域层关注实现领域对象的充血模型和聚合本身的原子业务逻辑,至于用户操作和业务流程,则交给应用层去编排。这样设计可以保证领域模型不容易受外部需求变化的影响,保证领域模型的稳定。
领域层的核心类:
通常,对于实体、值对象、聚合根,我们不可以不加类后缀,这样更能体现领域对象本身的含义。
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); }
主要负责技术细节处理,比如数据库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); } }