代码地址
闲话
几年前,我刚接触到DDD领域驱动,从网络上看了好多DDD相关的书籍、博客,也学习了很多DDD相关的开源代码。然而,不管我看多少DDD的书籍、博客、源码,DDD仍然是如迷雾笼罩,看不见虚实。
由于缘份,我开始看佛经,在看《金刚经》的时候,我从中领悟到“是名而非”四个字。于是,再回头看DDD领域驱动设计,那层终年笼罩的迷雾终于逐渐散去。
DDD是一门软件工程的方法论,是一种哲学思想。DDD没有具体的落地标准,更没有条条框框的教条主义,不同人、不同的角色,公司高层管理、项目经理、产品经理、市场营销,不同的视角,看到的DDD视角是不同的,而这些不同的视角,组合起来才是DDD真实的本质。
本文章,将从源代码的视角,描绘DDD。 阅读本文章,需要对DDD的一定程度了解。
1、源码架构
目录格式如下
root-dir
xxx-name-dir
xxx-name-application-dir
xxx-name-insfrastructure-dir
xxx-name-domian-dir
依赖关系如下
xxx-name-application依赖xxx-name-domian、也依赖xxx-name-insfrastructure,xxx-name-insfrastructure依赖xxx-name-domian,而xxx-name-domian啥也不依赖(只依赖如JDK之类的)。
xxx-name-domian是领域层,是业务相关的核心代码,不能包含mybaties、mysql、rocketmq之类的实现细节,极端的要求下,甚至连springboot之类的框架也不能依赖。
实体、值对象、领域服务、资源库、领域事件,都属于领域层,存放于domain中,而dto、enums、datacover这些服务于领域层的组件,自然也放在了domain中。
本项目,领域层的依赖是极端要求的,不会依赖springboot框架。
领域层之所以不依赖springboot框架,是因为,springboot框架也是技术实现细节。试想一下,如果突然有一天,springboot框架因为性能问题无法满足业务需求,而云原生的明星之子Quarkus正好能满足性能需求,那么,领域层因为不依赖springboot框架而可以直接复用,只需要改动application层。
xxx-name-application是应用层,存放controller、bo、vo(前端响应)、service(应用服务)、event(事件消费者)等。Application这个启动类,也是存放于application中。
xxx-name-insfrastructure是基于设施层,存放rocketmq、redis、mysql、mybaties、openfeign等细节技术实现。
2、实体、值对象,并不等于PO对象
很多开发人员,都自然而然觉得实体、值对象都会与数据库表一一对应,例如实体中有属性A,那么数据库表中也一定会有字段A,这是错误的!
PO对象才是与数据库表进行映射的,实体、值对象并不等于PO!
实体最终可以存储于Redis、也可以存储MogoDB、ES,并不一定得存放于数据库。
比如,实体中有个字段叫name,而数据库中却可以是两个字段firstName + lastName。
比如,一个UserEntity,需要t_user、t_user_person共同保存。
3、值对象,真的不需要保存到数据库吗?
值对象与实体的唯一区别,就是实体在生命周期内可反复被修改,而值对象一但创建之后就不能修改。
在领域建模的时候,一些业务对象,有的被建模成实体,有的被建模成值对象。在《领域驱动设计》一书中,作者推荐将业务实体建模成值对象,而不是实体。
审计日志,操作日志,这些日志化相关的数据,一但生成后就不能被修改,因此很适合建模成值对象。这些日志数据,需要在特定的场景下展示给用户。因此,必须对值对象进行持久化。
值对象的存储方式,和实体没有本质的区别。
4、怎么保存值对象不被修改?
值对象具有不可被修改的性质,因此,在值对象类中,不能有set之类的修改方法。
然而,值对象不对有set方法,会给开发人员带来很多麻烦。例如,前端参数UserBO,领域层创建User的时候,接收的是UserVO值对象,传统的做法是直接使用BeanUtil.copyProperties方法将UserBO转化成UserVO,但是因为UserVO中不能有set方法,BeanUtil.copyProperties转化失败。
BO与VO对象的转换,是难住广大开发人员使用值对象的重要原因。
对此,本文章的解决方案是:
VO中提供Builder,再使用MapStruct框架实现属性复制。
5、在实体中,怎么样发布领域事件?
Controller接收到前端请求,调用ApplicationService,再调用UserEntity创建用户,整个过程,就生成了UserCreatedEvent。那么,就生成了UserCreatedEvent到底是在UserEntity中发送,还是在ApplicationService中发送?
我觉得这是个无需讨论的问题。
领域事件是领域的一分子,脱离了领域,领域事件就无法产生,但是没有Controller、没有ApplicationService,领域事件是可以产生的。
实体是处于领域层,值对象也是属于领域层,领域事件也是属于领域层,领域事件的发送实现却是属于基础设施层。
那么,怎么样才能在实体内,实现领域事件的发布呢?
本文章的解决方案是:
将DomainEventPublisher放置于领域层,将DomainEventPublisherImpl放置于基础设施层,Entity中使用DomainEventPublisherFactory来获取DomainEventPublisher的具体实现类(依赖反转)
注意的是DomainEventPublisher理所当然是属于领域层的,因为,领域事件属于领域层,而事件对应的发布器,也必定是属于领域层,事件发布器是个抽象,而不是具体实现。