DDD的落地,主要有三个方面需要理清:第一,以何种应用架构进行落地;第二,核心组件及其生命周期,相互之间的交互逻辑;第三,不同限界上下文之间如何集成。
本文将重点分析DDD落地的应用架构,其余两个方面后续会专门去展开。
DDD的实现架构有很多,有经典四层架构、六边形(适配器端口)架构、整洁架构(clean architecture)、CQRS架构等,相信很多读者跟我刚开始接触时一样,完全不知道该选择什么架构进行落地。
本文不会逐个去讲解这些架构,而是从我们日常的三层架构出发,带领大家思考适合我们落地的架构。
我们很多项目是基于三层架构的,其结构如图:
我们说三层架构,为什么还画了一层Model呢?因为Model只是简单的Java Bean,里面只有数据库表对应的属性,有的应用会将其单独拎出来作为一个maven模块。
接下来我们开始对这个三层架构进行抽象精炼。
为什么数据模型要与数据访问层合并呢?首先,数据模型与数据库表结构的字段是一一对应的,数据模型最主要的应用场景就是持久层用来进行ORM,给Service层返回封装好的数据模型,供Service执行业务;其次,数据模型的Class或者属性字段上,通常带有ORM框架的一些注解,跟持久层联系非常紧密,根据单一职责的原则,我认为数据模型就是持久层拿来查询或者持久化数据的,数据模型脱离了持久化层,意义不大。
常见的Service方法,既有缓存、数据库的调用,也有实际的业务逻辑,整体过于臃肿。
先看现在的Service方法的伪代码:
public void businessLogic(Param param){ if(checkParam(param)){ throw new XXXException(); } Data data=new Data();//或者是mapper.queryOne(param); data.setId(param.getId()); if(condition1=true){ biz1=biz1(param.getProperty1()) data.setProperty1(biz1); }else{ biz1=biz11(param.getProperty1()) data.setProperty1(biz1); } if(condition2=true){ biz2=biz2(param.getProperty2()) data.setProperty2(biz2); }else{ biz2=biz22(param.getProperty2()) data.setProperty2(biz2); } //一堆set方法 …… mapper.updateXXXById(data); cache.del(data);//删除缓存 mpPublisher.publish(data);//发出消息}
这是典型的事务脚本的代码:先做参数校验,然后通过biz1、biz2等子方法做业务,并将其结果通过一堆Set方法设置到数据模型中,再将数据模型更新到数据库。
由于所有的业务逻辑都在Service方法中,造成Service方法非常臃肿,Service需要了解所有的业务规则,并且要清楚如何将基础设施串起来。同样的一条规则,例如if(condition1=true),很有可能在每个方法里面都出现。
我们知道,专业的事情就该让专业的人干。既然业务逻辑是跟具体的业务场景相关的,我们想办法把业务逻辑提取出来,形成一个模型,让这个模型的对象去执行具体的业务逻辑,即提供业务能力。这样,Service方法就不用再关心里面的if/else业务规则,只需要给业务模型执行的舞台,并提供基础设施完成用例即可。
将业务逻辑形成模型了,这样的模型,就是领域模型。
我们先不管领域模型怎么得到,总之,拿到Service方法的入参之后,我们通过某种途径得到一个模型,我们让这个模型去做业务逻辑,最后执行的结果也都在模型里,我们再将模型写回数据库,当然,怎么写数据库的我们也先不管。
抽取之后,将得到如下的伪代码:
public void businessLogic(Param param){ if(checkParam(param)){ throw new XXXException(); } Domain domain=loadDomain(param); domain.doBusinessLogic(); saveDomain(domain); cache.del(domain);//删除缓存 mpPublisher.publish(domain);//发出消息}
根据代码,我们已经将业务逻辑抽取出来了,领域相关的业务规则封闭在领域模型内部。此时Service方法非常直观,就是获取模型、执行业务逻辑、保存模型,再协调基础设施完成其余的操作。
抽取完领域模型后,我们工程的结构如下图:
在第二步中,loadDomain、saveDomain两个方法还没有得到讨论,这两个方法跟领域对象的生命周期息息相关。
不管是loadDomain还是saveDomain,我们一般都要依赖于数据库或者其他中间件,所以这两个方法对应的逻辑,肯定是要跟DAO产生联系的。而保存或者加载领域模型,我们可以抽象成一种组件,这种组件就是Repository。
注意,Repository加载或者保存领域模型,方法的入参或者出参,一定是基本数据类型或者领域内定义的类型,而不能是表数据模型。
以下是Repository的伪代码:
public interface DomainRepository{ void save(Domain domain); Domain load(DomainId id);//假设是领域模型ID,也可能是其他的入参}
既然DomainRepository与底层数据库有关联,但是我们现在DAO层并没有引入Domain这个包,DAO层自然无法提供DomainRepository的实现,我们初步考虑可以将这个DomainRepository实现在Service中。
我们再推敲推敲,如果我们在Service中实现,势必需要在Service中操作数据模型:查询出来数据模型再封装为领域模型、或者将领域模型转为数据模型再通过ORM保存,但这个过程不该是Service层该关心的。
所以,我们决定在DAO层直接引入Domain包,并在DAO层提供DomainRepository接口的实现,DAO层的Mapper查询出数据模型之后,封装成领域模型供DomainRepository返回。
此时DAO层不再向Service返回数据模型,而是返回领域模型。
现在,我们项目的架构图是这样的了:
在第三步中,我们的架构图已经跟经典四层架构非常相似了,我们再把格局放大点,对整个架构进行泛化抽象。
首先,DAO层其实属于基础设施层,只不过其职责是持久化和加载聚合,所以,我们将DAO层改名为Infrastructure-Persistence,可以理解为基础设施层持久化包。之所以采取这种Infrastructure-XXX的格式进行命名,是由于Infrastructure可能会有很多的包,分别提供不同的基础设施支持。一般的项目,还有可能需要引入缓存,例如Redis,我们就可以再加一个包,名字叫Infrastructure-Cache或者Infrastructure-Redis。
注意:为了确保领域层知识的完备,Infrastructure层应该实现Domain层定义的基础设施接口,而不是向上层提供没有在领域层定义过的组件。
然后,Controller层其实就是用户接口层,即User Interface层,我们在项目叫Interfaces。Controller层的名字有很多,有的叫Rest,有的叫Resource,考虑到我们这一层不只是有Rest接口,还可能还有一系列的拦截器,所以我一般比较随意的称之为Web。因此,我们将其改名为Interfaces-Web,即用户接口层的Web包。同样,我们可能会有很多的用户接口,但是他们通过不同的协议对外提供服务,因而被划分到不同的包中。我们如果有对外提供的RPC服务,那么其服务实现类所在的包就可以命名为Interfaces-Provider。
有时候引入某个中间件,既会增加Infrastructure也会增加Interfaces。例如,如果引入Kafka就需要考虑一下,如果是给Service层提供调用的,例如逻辑执行完发送消息通知下游,那么我们再加一个包Infrastructure-Publisher;如果是消费Kafka的消息,然后调用Service层执行业务逻辑的,那么就可以命名为Interfaces-Subscriber。
最后,Service层目前已经没有业务逻辑了,业务逻辑都在Domain层去执行了,Service只是提供了应用服务,用于将领域的业务逻辑、领域服务封装为用例,所以,我们把Service层改名为Application层。
经过第四步的抽象,其架构图为:
在这个架构图中,经典四层架构的四层都出现了,整个架构图长得跟六边形架构也很像。这是为什么呢?其实,不管是经典四层架构、还是六边形架构,亦或者整洁架构,都是对系统应用的描述,也许描述的侧重点不一样,但是描述的是同一个事物。既然描述的是同一个事物,长得像才是理所当然的。
对于任何一个应用,都可以看成“输入-处理-输出”的过程。
“输入”环节:通过某种协议对外暴露领域的能力,这些协议可能是REST、可能是RPC、可能是MQ的监听器,也可能是WebSocket,也可能是一些任务调度的Task;
”处理“环节:处理环节是整个应用的核心,代表了应用具备的核心能力,是应用的价值所在,应用在这个环节执行业务逻辑。
“输出”环节,业务逻辑执行完成之后将结果输出到外部。
不管我们采用的什么架构,其描述的应用的核心都是这个过程。正如《金刚经》所言:一切有为法,如梦幻泡影,如露亦如电,应作如是观;凡有所相,皆是虚妄;若见诸相非相,即见如来。
将第四步中出现的包进行整理,并加入启动包,则得到如下完整的包结构。
由于有很多的User Interface,所以启动类应该存放在单独的模块中,又因为application这个名字被应用层占用了,所以将启动类所在的包命名为launcher。
包结构如图所示:
至此,DDD项目的整体结构基本讲完了。
(未完待续)
留言与评论(共有 0 条评论) “” |