DDD (Domain-Driven Design 领域驱动设计) 或许也叫 Dream-Driven Design,某度说这是一种程序的设计思想,使用诸如聚合根,值对象,六边形架构,CQRS(命令和查询职责分离),事件驱动等等概念,在领域专家的指引下构造代码。在这种情况下写出的代码具备领域划分明确,响应需求变更快等特点。
但在目前大部分情况下都是用来吹逼的东西,概念虚无缥缈,实践中困难重重。
因为最近公司业务需要,我从一个只写java的码农需要逐渐转变成一个要写一部分golang的码农。
在熟悉了spring-boot各种封装特性以及刻在骨子里的MVC思想的我开始接触go — 这语法是真的丑
大约爬了2、3天代码,我发现出大问题,似乎没了MVC,就不会写代码了,更恐怖的是我发现正统golang出身的码农眼里似乎根本就没有MVC,他们写代码很多像是放飞自我型,5个人里面可能有6种风格,而我的代码似乎是第7种。
没了MVC,代码感觉失去了灵魂,那么除了MVC,还有什么是可以去指引代码结构的呢?怀着这种疑问,我发现了DDD
传统MVC结构写代码基本思路和面向过程没太大区别,以典型springboot项目为例,一个接口的调用链大概是这样的
controller -> service -> dao
基本遵循一个A -> B -> C 函数间调度,实体类entity基本都是当做入参来传递。与面向对象3大特性:封装、继承、多态几乎不沾边,写代码如同填空题,只需要往里面填东西就可以了
百度告诉我,DDD基本都采取面向对象的编程思路,在写的过程中要求针对业务对类进行职责分明的封装,并且需要将不同类的领域边界划分清楚,一个类体现的是"业务的高度浓缩"
,也就是一个类既具备能力也具备属性。DDD追求的是核心代码使用原生的代码,不依赖任何第三方库或者框架,这样易于维护和迁移。DDD的代码往往采用聚合根,值对象,六边形架构,CQRS(命令和查询职责分离),事件驱动等等高大上的手法,可以让我的代码无比完美。
所以我随手拿了一个曾经的crud项目作为样例,开始了改造。
将贫血模型改成充血模型,简单点就是把service和dao这2层合并,将方法和私有成员放在一起,再按照业务重新划分对象边界,把新生的类丢到一个包下面,最后给它取一个名字 — 聚合根
大概就是原来长这样
public class OrderModel {
private Long id;
...
}
现在长这样
public class Order {
private Long id;
...
public Long getOrderKey() {
// 此处省略很多拼装逻辑
return SHA256(id);
}
...
}
将贫血模型改成充血模型之后,面临了2个问题
为了解决这2个问题,我不得不将spring的生存空间压缩到controller层,只用spring封装的request入口,在聚合根的方法里加入了大量的数据校验
数据持久化需要单独抽离,写在一个叫repository的包下面,通过repository和聚合根本身或聚合根的数据的组合实现对数据库的crud工作。
大概是原来长这样
public Long addOrder(RequestDTO request) {
// 此处省略很多拼装逻辑
OrderDO orderDO = new OrderDO();
orderDAO.insertOrder(orderDO);
return orderDO.getId();
}
public void updateOrder(OrderDO orderDO, RequestDTO updateRequest) {
orderDO.setXXX(XXX); // 省略很多
orderDAO.updateOrder(orderDO);
}
public void doSomeBusiness(Long id) {
OrderDO orderDO = orderDAO.getOrderById(id);
// 此处省略很多业务逻辑
}
现在长这样
@Repository
public class OrderRepositoryImpl implements OrderRepository {
private final OrderDAO dao;
private final OrderDataConverter converter;
public OrderRepositoryImpl(OrderDAO dao) {
this.dao = dao;
this.converter = OrderDataConverter.INSTANCE;
}
@Override
public Order find(OrderId orderId) {
OrderDO orderDO = dao.findById(orderId.getValue());
return converter.fromData(orderDO);
}
@Override
public void remove(Order aggregate) {
OrderDO orderDO = converter.toData(aggregate);
dao.delete(orderDO);
}
@Override
public void save(Order aggregate) {
if (aggregate.getId() != null && aggregate.getId().getValue() > 0) {
// update
OrderDO orderDO = converter.toData(aggregate);
dao.update(orderDO);
} else {
// insert
OrderDO orderDO = converter.toData(aggregate);
dao.insert(orderDO);
aggregate.setId(converter.fromData(orderDO).getId());
}
}
@Override
public Page<Order> query(OrderQuery query) {
List<OrderDO> orderDOS = dao.queryPaged(query);
long count = dao.count(query);
List<Order> result = orderDOS.stream().map(converter::fromData).collect(Collectors.toList());
return Page.with(result, query, count);
}
@Override
public Order findInStore(OrderId id, StoreId storeId) {
OrderDO orderDO = dao.findInStore(id.getValue(), storeId.getValue());
return converter.fromData(orderDO);
}
}
可以看到在使用repository模式之后,又面临了1个问题
将所有事情分成查询类和变更类:
查询类的事情由controller接到,直接构造repository对象调用查询方法返回即可。
变更类就比较麻烦了,先发出一个事件记录变更的开始,事件处理时编写业务构造聚合根,repository等对象,如果需要多个聚合根进行链式变更,则需要处理完一次变更后再发出一个事件通知下一个,直到事件完全结束。
很明显,变更类的事件驱动的调用链建立在一个又一个的事件传递上,那么我们就必须要有一个机制保证事件的连续和最终一致。比较典型的做法就是建立事件中心,提供回滚机制等。
改造到现在,基本已经完成DDD所要求的雏形,整个调用逻辑就是借助springboot为我们提供的controller接到request对象,开始按接口职责不同分为
如果是查询类的接口就通过repository的接口查询数据库返回数据,中间或许需要经历 dto对象转entity对象转do对象查询,最后查出的结果可能还得转回来。
如果是变更类接口就通过事件驱动,一个事件一个事件的处理,为了保证事件的最终一致,那么需要建立事件中心,提供回滚机制等额外措施。
在初步实践之后,我似乎发现,DDD思想有一个死穴
那就是所有的一切,都建立在领域的划分上
如果这个领域划分的不正确呢?如果有代码侵入到别人的领域呢?如何保证项目组所有人都能理解这个领域划分,大家各司其职不会犯错?
人总是会犯错的,特别是在上线日期临近的时候,往往会打破规范走捷径,在这个写法下,只要一步错,未来就会步步错,DDD就名存实亡了。
个人认为,使用DDD进行核心代码的编写需要满足以下的条件
团队想使用DDD的思想进行开发,要具备一个稳定的高素质的团队,团队内每个人的技术能力、视野比较相近,每个人对业务都要有深入的理解。拥有一个能力足够强大的架构师,可以将业务分解成界限分明的模块,把控代码的质量。
DDD是个好东西,但也是个走钢丝的活,在一群大佬手上确实可以,例如kubernetes就是经典的DDD风格的架构设计,但不是所有团队都是Google,是阿里,是腾讯,能时时刻刻盯着你的代码不让项目走偏。真正普世的架构或许就是朴实无华的MVC,它简单明了,上手极快,问题易于处理,是那种真正做到是人是妖都要被框在大工程的进度中发挥正向的作用。
ps: github上stars>10000的Java业务项目里面,几乎找不到一个使用DDD进行开发的,都是标准的MVC思想,代码基本都是朴实无华的逻辑堆砌,或许这就是大型工程类项目的最有效方式。
参考: https://www.colabug.com/2020/0702/7488532/