三层架构到DDD四层架构演进

2023-10-27

应用架构演化

三层架构:

三层架构,为什么还画了一层 Model 呢?因为 Model 只是简单的 Java Bean,里面只有数据库表对应的属性,有的应用会将其单独拎出来作为一个,但实际上可以合并到 DAO 层。

第一步、数据模型与DAO层合并

为什么数据模型要与DAO层合并呢?

首先,数据模型是贫血模型,数据模型中不包含业务逻辑,只作为装载模型属性的容器;

其次,数据模型与数据库表结构的字段是一一对应的,数据模型最主要的应用场景就是DAO层用来进行 ORM,给 Service 层返回封装好的数据模型,供Service 获取模型属性以执行业务;

最后,数据模型的 Class 或者属性字段上,通常带有 ORM 框架的一些注解,跟DAO层联系非常紧密,可以认为数据模型就是DAO层拿来查询或者持久化数据的,数据模型脱离了DAO层,意义不大。

第二步、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层的方法经过抽取之后,将得到如下的伪代码:

public class Service {

    public void bizLogic(Param param) {

        //如果校验不通过,则抛一个运行时异常
        checkParam(param);
        //加载模型
        Domain domain = loadDomain(param);
        //调用外部服务取值
	    SomeValue someValue=this.getSomeValueFromOtherService(param.getProperty2());
        //模型自己去做业务逻辑,Service不关心模型内部的业务规则
        domain.doBusinessLogic(param.getProperty1(), someValue);
        //保存模型
        saveDomain(domain);
    }
}

根据代码,已经将业务逻辑抽取出来了,领域相关的业务规则封闭在领域模型内部。此时 Service方法非常直观,就是获取模型、执行业务逻辑、保存模型,再协调基础设施完成其余的操作。

抽取完领域模型后,工程的结构如下图:

第三步、维护领域对象生命周期

在上一步中,loadDomainsaveDomain 这两个方法还没有得到讨论,这两个方法跟领域对象的生命周期息息相关。

关于领域对象的生命周期的详细知识,可以自行学习了解。

不管是 loadDomain 还是 saveDomain,我们一般都要依赖于数据库,所以这两个方法对应的逻辑,肯定是要跟 DAO 产生联系的。

保存或者加载领域模型,可以抽象成一种组件,通过这种组件进行封装模型加载、保存的操作,这种组件就是Repository。

注意,Repository 是对加载或者保存领域模型(这里指的是聚合根,因为只有聚合根才会有Repository)的抽象,必须对上层屏蔽领域模型持久化的细节,因此其方法的入参或者出参,一定是基本数据类型或者领域模型,不能是数据库表对应的数据模型。 例:返回对象是PageInfo(mybatis的分页插件),持久层改为ES时,需要添加新的方法,而不是再加一个实现类

【聚合根】仅包含对象和行为?

以下是 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。

现在,项目的架构图是这样的了:

由于数据模型属于贫血模型,自身没有业务逻辑,并且只有Repository这个包会用到,因此将之合并到Repository中,接下来不再单独列举。

第四步、泛化抽象

在第三步中,架构图已经跟经典四层架构非常相似了,再对某些层进行泛化抽象。

  • Infrastructure

Repository 仓储层其实属于基础设施层,只不过其职责是持久化和加载聚合,所以,将 Repository层改名为 infrastructure-persistence,可以理解为基础设施层持久化包。

之所以采取这种 infrastructure-XXX 的格式进行命名,是由于 Infrastructure 可能会有很多的包,分别提供不同的基础设施支持。

例如:一般的项目,还有可能需要引入缓存,就可以再加一个包,名字叫infrastructure-cache

对于外部的调用,DDD中有防腐层的概念,将外部模型通过防腐层进行隔离,避免污染本地上下文的领域模型。使用入口(Gateway)来封装对外部系统或资源的访问(详细见《企业应用架构模式》,18.1入口(Gateway)),因此将对外调用这一层称之为infrastructure-gateway

注意:Infrastructure 层的门面接口都应先在Domain 层定义,其方法的入参、出参,都应该是领域模型(实体、值对象)或者基本类型。

  • User Interface

Controller 层其实就是用户接口层,即 User Interface 层,在项目简称 ui。当然了可能很多开发者会觉得叫UI好像很别扭,认为 UI就是 UI 设计师设计的图形界面。

Controller 层的名字有很多,有的叫 Rest,有的叫 Resource,考虑到这一层不只是有 Rest 接口,还可能还有一系列 Web相关的拦截器,所以我一般称之为 Web。因此,我们将其改名为 ui-web,即用户接口层的 Web 包。

同样,我们可能会有很多的用户接口,但是他们通过不同的协议对外提供服务,因而被划分到不同的包中。

如果有对外提供的 RPC服务,那么其服务实现类所在的包就可以命名为 ui-provider

有时候引入某个中间件会同时增加 Infrastructure 和 User Interface。

例如,如果引入 Kafka 就需要考虑一下,如果是给 Service 层提供调用的,例如逻辑执行完发送消息通知下游,那么我们再加一个包infrastructure-publisher;如果是消费 Kafka 的消息,然后调用 Service 层执行业务逻辑的,那么就可以命名为 ui-subscriber

  • Application

至此,Service 层目前已经没有业务逻辑了,业务逻辑都在 Domain 层去执行了,Service 只是协调领域模型、基础设施层完成业务逻辑。

所以,把 Service 层改名为 Application Service 层。

经过第四步的抽象,其架构图为:

第五步、完整的包结构

继续对第四步中出现的包进行整理,此时还需要考虑一个问题,启动类应该放在哪里?

由于有很多的 User Interface,所以启动类放在任意一个User Interface中都不合适,放置在Application Service中也不合适,因此,启动类应该存放在单独的模块中。又因为 application这个名字被应用层占用了,所以将启动类所在的模块命名为 launcher,一个项目可以存在多个launcher,按需引用User Interface。

加入启动包,就得到了完整的 maven 包结构。

包结构如图所示:

至此,DDD 项目的整体结构基本完成了。

第六步、精炼后的思考

在经过前面五步精炼得到这个架构图中,经典四层架构的四层都出现了,而且长得跟六边形架构也很像。这是为什么呢?

其实,不管是经典四层架构、还是六边形架构,亦或者整洁架构,都是对系统应用的描述,也许描述的侧重点不一样,但是描述的是同一个事物。既然描述的是同一个事物,长得像才是理所当然的,不可能只是换一个描述方式,系统就从根本上发生了改变。

对于任何一个应用,都可以看成“输入-处理-输出”的过程。

“输入”环节:通过某种协议对外暴露领域的能力,这些协议可能是 REST、可能是 RPC、可能是 MQ 的订阅者,也可能是WebSocket,也可能是一些任务调度的 Task;

”处理“环节:处理环节是整个应用的核心,代表了应用具备的核心能力,是应用的价值所在,应用在这个环节执行业务逻辑,贫血模型由Service执行业务处理,充血模型则是由模型进行业务处理

“输出”环节,业务逻辑执行完成之后将结果输出到外部。

不管采用的什么架构,其描述的应用的核心都是这个过程,不必生搬硬套非得用什么应用架构。

参考资料:https://juejin.cn/post/7238254724864294970?utm_source=gold_browser_extension

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

三层架构到DDD四层架构演进 的相关文章

随机推荐

  • 左程云算法笔记(三)堆排序、桶排序、排序总结

    左程云算法笔记 三 堆 堆结构 堆排序 堆排序扩展题 桶排序 计数排序 基数排序 补 桶排序 补 希尔排序 排序总结 稳定性 综合比较 常见的坑 工程上对排序的改进 堆 堆结构 1 堆结构就是用数组实现的完全二叉树结构 2 完全二叉树中如果
  • Spring Boot使用(基础)

    目录 1 Spring Boot是什么 2 Spring Boot使用 2 1Spring目录介绍 2 2SpringBoot的使用 1 Spring Boot是什么 Spring Boot就是Spring脚手架 就是为了简化Spring开
  • 权重设计介绍

    相关产品 RocketMQ 之前在阿里的 负载均衡 比赛中 就有这个算法 RocketMQ 中包含多种负载算法 其中 权重 算法就属于其中一种 也是最实用的一种 奖励系统 经常会有许多活动 或则任务 当用户完成后 可以获得相关的奖励 当然可
  • 华为OD机试 - 评论转换输出(Java)

    题目描述 在一个博客网站上 每篇博客都有评论 每一条评论都是一个非空英文字母字符串 评论具有树状结构 除了根评论外 每个评论都有一个父评论 当评论保存时 使用以下格式 首先是评论的内容 然后是回复当前评论的数量 最后是当前评论的所有了评论
  • 操作系统内核

    现代操作系统一般将OS划分为若干个层次 再将OS的不同功能分别设置在不同的层次中 通常将一些与硬件紧密相关的模块 如中断处理程序等 各种常用及运行频率较高的模块 如时钟管理 进程调度和许多模块所公用的二些基本操作 都安排在紧靠硬件的软件层次
  • vue生命周期

    随着对 vue 的不断了解 会越来越发现它生命周期的重要性 只有了解了它的生命周期 才能在开发项目的时候在逻辑上的很好的判断什么时候该发生什么事件 即很好的控制页面 一 什么是 vue 生命周期 Vue 实例从创建到销毁的过程 就是生命周期
  • kong+Verdaccio+ldap(docker)

    需求 使用Kong来代理Verdaccio 实现直接通过域名 path的方式来访问 并且Verdaccio使用ldap来管理 前提 Kong ldap已部署好 并且也部署好了Kong Dashboard 后面有需求再写关于Kong代理的各种
  • 国内的Ubuntu镜像源

    国内的Ubuntu镜像源 Ubuntu清华镜像源 今天学习docker需要在线Ubuntu镜像 所以做了一个镜像下载地址笔记 方面以后的下载 官方镜像下载访问地址 https cn ubuntu com download alternati
  • 明哥复习MyBatis(1)

    这不是原创 借鉴尚硅谷的 不是原创 一 核心配置文件详解
  • impdp或expdp报错ORA-39002: invalid operation,ORA-39070: Unable to open the log file

    oracle oracle rac01 backup cat expdp par USERID as sysdba DIRECTORY KMDATA EXP job name jinky exp filesize 5000m paralle
  • Java集合——Iterable和Iterator接口介绍

    Iterable Iterable和Iterator是什么 Iterable源码 Iterator源码 使用 Iterable和Iterator是什么 Iterable是Collection的实现接口 即是集合的最顶级父类 Iterator
  • 小白学股票基金_2

    到底是买股票还是买基金 说基金和股票差不多指的应该是偏股型基金 但是基金里面还有债券型 QDII以及非常大众化的货币型基金 众所周知的余额宝就是一款货币型基金啦 等等 风险等级也不一样 所以 为防止这样混为一谈 我们这里讨论的就是炒股和买偏
  • 0.0 Windows + Linux(Ubuntu20.04) 超简单的双系统安装

    目录 一 U盘启动盘的制作 1 下载操作系统 2 下载U启动制作工具 二 磁盘分配 三 Ubuntu20 04系统安装 1 修改bios为U启动 2 安装配置Ubuntu 2 1 Install Ubuntu 2 2 安装关键之处在于分区
  • 4年经验来面试20K的测试岗,连基础都不会,还不如招应届生!

    公司前段时间缺人 也面了不少测试 结果竟然没有一个合适的 一开始瞄准的就是中级的水准 也没指望来大牛 提供的薪资在10 20k 面试的人很多 但平均水平很让人失望 看简历很多都是3 4年工作经验 但面试中 不提工具和编程 仅仅基础的技术很多
  • Flink简单教学5-时间

    时间 Time Flink支持三种不同的时间 处理时间 processing time 指当前操作的时间 如 map 当程序以处理时间运行 所有基于时间的操作 窗口 都依赖各个operator的机器时间 事件时间 event time 事件
  • office 2010 projectn visio 下载

    Office 2010 project专业版 64位 ed2k file cn project professional 2010 x64 515551 exe 453437272 84D14496F889767D46C9DE576154B
  • 矩阵的基本演算

    目录 转置 逆 迹 行列式 转置 A B T
  • 网易低代码引擎Tango正式开源

    一 Tango简介 Tango 是一个用于快速构建低代码平台的低代码设计器框架 借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建 Tango 低代码设计器直接读取前端项目的源代码 并以源代码为中心 执行和渲染前端
  • element ui Tag 动态添加标签

    1 首先调接口获取到标签列表 级联选择器如果选择的不是第三级菜单 async getParamsData if this selectedCateKeys length 3 级联选择器不会有参数 下面的面板也都为空 this selecte
  • 三层架构到DDD四层架构演进

    应用架构演化 三层架构 三层架构 为什么还画了一层 Model 呢 因为 Model 只是简单的 Java Bean 里面只有数据库表对应的属性 有的应用会将其单独拎出来作为一个 但实际上可以合并到 DAO 层 第一步 数据模型与DAO层合