我们说三层架构,为什么还画了一层 Model 呢?因为 Model 只是简单的 Java Bean,里面只有数据库表对应的属性,有的应用会将其单独拎出来作为一个
Maven Module,但实际上可以合并到 DAO 层。接下来我们开始对这个三层架构进行抽象精炼。2.1 第一步、数据模型与DAO层合并
为什么数据模型要与DAO层合并呢?首先,数据模型是贫血模型,数据模型中不包含业务逻辑,只作为装载模型属性的容器;其次,数据模型与数据库表结构的字段是一一对应的,数据模型最主要的应用场景就是DAO层用来进行 ORM,给 Service 层返回封装好的数据模型,供Service 获取模型属性以执行业务;最后,数据模型的 Class 或者属性字段上,通常带有 ORM 框架的一些注解,跟DAO层联系非常紧密,可以认为数据模型就是DAO层拿来查询或者持久化数据的,数据模型脱离了DAO层,意义不大。2.2 第二步、Service层抽取业务逻辑
下面是一个常见的 Service 方法的伪代码,既有缓存、数据库的调用,也有实际的业务逻辑,整体过于臃肿,要进行单元测试更是无从下手。
public class Service {
@Transactional
public void bizLogic(Param param) {
checkParam(param);//校验不通过则抛出自定义的运行时异常
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);
}
}
复制代码
这是典型的事务脚本的代码:先做参数校验,然后通过 biz1、biz2 等子方法做业务,并将其结果通过一堆 Set 方法设置到数据模型中,再将数据模型更新到数据库。由于所有的业务逻辑都在 Service 方法中,造成 Service 方法非常臃肿,Service 需要了解所有的业务规则,并且要清楚如何将基础设施串起来。同样的一条规则,例如if(condition1=true),很有可能在每个方法里面都出现。专业的事情就该让专业的人干,既然业务逻辑是跟具体的业务场景相关的,我们想办法把业务逻辑提取出来,形成一个模型,让这个模型的对象去执行具体的业务逻辑。这样Service方法就不用再关心里面的 if/else 业务规则,只需要通过业务模型执行业务逻辑,并提供基础设施完成用例即可。将业务逻辑抽象成模型,这样的模型就是领域模型。要操作领域模型,必须先获得领域模型,但此时我们先不管领域模型怎么得到,假设是通过loadDomain方法获得的。通过 Service方法的入参,我们调用loadDomain方法得到一个模型,我们让这个模型去做业务逻辑,最后执行的结果也都在模型里,我们再将模型回写数据库。当然,怎么写数据库的我们也先不管,假设是通过saveDomain方法。Service层的方法经过抽取之后,将得到如下的伪代码:
在上一步中,loadDomain、saveDomain 这两个方法还没有得到讨论,这两个方法跟领域对象的生命周期息息相关。关于领域对象的生命周期的详细知识,读者可以自行学习了解。不管是 loadDomain 还是 saveDomain,我们一般都要依赖于数据库,所以这两个方法对应的逻辑,肯定是要跟 DAO 产生联系的。保存或者加载领域模型,我们可以抽象成一种组件,通过这种组件进行封装模型加载、保存的操作,这种组件就是Repository。注意,Repository 是对加载或者保存领域模型(这里指的是聚合根,因为只有聚合根才会有Repository)的抽象,必须对上层屏蔽领域模型持久化的细节,因此其方法的入参或者出参,一定是基本数据类型或者领域模型,不能是数据库表对应的数据模型。以下是 Repository 的伪代码:
public interface DomainRepository {
void save(AggregateRoot root);
AggregateRoot load(EntityId id);
}
复制代码
接下来我们要考虑在哪里实现DomainRepository。既然 DomainRepository 与底层数据库有关联,但是我们现在 DAO 层并没有引入 Domain 这个包,DAO 层自然无法提供 DomainRepository的实现,我们初步考虑是不是可以将 DomainRepository 实现在 Service 层。但是,如果我们在 Service 中实现DomainRepository,势必需要在 Service 层操作数据模型:查询出来数据模型再封装为领域模型、或者将领域模型转为数据模型再通过ORM 保存,这个过程不该是 Service 层关心的。因此,我们决定在 DAO 层直接引入 Domain 包,并在 DAO 层提供 DomainRepository 接口的实现,DAO 层查询出数据模型之后,封装成领域模型供DomainRepository 返回。这样调整之后, DAO 层不再向 Service 返回数据模型,而是返回领域模型,这就隐藏了数据库交互的细节,我们也把DAO层换个名字称之为Repository。现在,我们项目的架构图是这样的了: