关于mvc:从MVC到DDD该如何下手重构

51次阅读

共计 9366 个字符,预计需要花费 24 分钟才能阅读完成。

作者:付政委
博客:bugstack.cn

积淀、分享、成长,让本人和别人都能有所播种!😄

大家好,我是技术 UP 主小傅哥。MVC 解说了,DDD 解说了。接下来这个章节,咱们讲讲从 MVC 到 DDD 的重构!

MVC 旧工程腐化重大,迭代老本太高。DDD 新工程全副重构,步子扯的太大。 这是现阶段在工程体系化治理中,咱们所面临的最大问题;既想使用 DDD 的思维循序渐进重构现有工程,又想不毁坏原有的工程体系结构以放弃新需要的承接效率。

通过实际得悉,DDD 架构能解决,现阶段 MVC 贫血构造中所遇到的泛滥问题。

家喻户晓,MVC 分层构造是一种贫血模型设计,它将”状态“和”行为“拆散到不同的包构造中进行开发应用。domain 里写 po、vo、enum 对象,service 里写性能逻辑实现。也正因为 MVC 构造没有太多的束缚,让后期的交付速度十分快。但随着系统工程的长期迭代,贫血对象开始被泛滥 serivice 穿插应用,而 service 服务也是互相调用。这样短少一个上下文关系的开发方式,让长期迭代的 MVC 工程逐渐腐化到重大腐化。

MVC 工程的腐化基本,就在于对象、服务、组件的穿插凌乱应用。工夫越长,腐化的越重大。

在 MVC 的分层构造就像家里所有人的衣服放一个大衣柜、所有人的裤子放一个大库柜。衣服裤子 (对象),很少的时候很节俭空间, 因为你的裤子他人可能也拿去穿,复用一下开发速度很快。但工夫一长,就越来越乱了。🤨 一条裤子被加肥加大,所有人都穿。

而 DDD 架构的模型分层,则是以人为视角,一个人就是一个畛域,一个畛域内包含他所需的衣服、裤子、袜子、鞋子。尽管刚开始有点节约空间,但随着软件的长周期倒退,后续的保护老本就会升高。

那么,接下来咱们就着重看以下,从 MVC 到 DDD 的轻量化重构应该怎么做。🍻

文章前面,含有 MVC 到 DDD 重构编码实际解说。此文也是 MVC、DDD 的架构编码领导教训阐明。

一、能学到啥

本文是偏实战可落地的 DDD 常识分享,也是从 MVC 到 DDD 的可落地计划解说。在本文中会介绍 DDD 架构下的分层构造、调用全景图以及十分重要的 MVC 到 DDD 应该如何映射和编码。所以如下这一系列内容都是你能取得的常识;

  1. DDD 畛域驱动设计,对应的分层构造解说。涵盖调用关系、依赖关系、对象转换以及各层的性能划分。—— 简略且清晰。
  2. DDD 调用全景图,以一张全方位的构造关系调用视图,开展 DDD 的血脉流转关系。有了这一张视图,你会更加分明的晓得 DDD 的调用链路构造和各个代码都要写到那一层。
  3. MVC 映射 DDD 后的调整计划,在尽可能低的老本下,让 MVC 构造具备 DDD 畛域驱动设计的实现思维。这样的调整,能够在肯定水平上,阻止旧工程的腐化水平,进步编码品质。同时也为后续从 MVC 到 DDD 的迁徙,做好根底。
  4. MVC、DDD 是工程设计骨架,设计准则、设计模式是工程实现血肉。所以设计模式也是本文要展现的重点内容。
  5. 一整套实战开源课程;解说在 DDD 架构中,各项技术栈;Dubbo、MQ、Redis、Zookeeper – 配置核心等的分层应用。—— 否则你可能都不晓得一个 MQ 音讯发送要放在哪里。有了 DDD 分层架构,这些货色会被归类的特地清晰。

此外,除了这些碎片化的常识学习,还有利用级实战我的项目锤炼;Lottery DDD 架构设计、ChatGPT 新 DDD 架构设计、API 网关 会话设计 – 学习架构能力和编程思维,以及高端的编码技巧。

二、架构分层(DDD)

在 DDD 架构分层中,domain 模块最重要的,也是最大的那个。所有的其余模块都要围着它转。所有 domian 下的各个领域模块,都蕴含着一组残缺的;model – 模型对象、service – 服务解决,以及在有须要操作数据库时,再引入对应的 IRepository – 仓储服务。这个 domain 的实现,就像是实现了一个炸药包,炸药包的火药、引线、包布等都是一个个物料被封装到一起应用。

如下是 DDD 架构所呈现出的一种四层架构分层,可能和一些其余的 DDD 分层略有差别,但外围的重点构造是不变的。尤其是 domain 畛域、infrastructure 根底,是任何一个 DDD 架构分层都须要有的分层模块。

  • 利用封装 – app:这是利用启动和配置的一层,如一些 aop 切面或者 config 配置,以及打包镜像都是在这一层解决。你能够把它了解为专门为了启动服务而存在的。
  • 接口定义 – api:因为微服务中援用的 RPC 须要对外提供接口的形容信息,也就是调用方在应用的时候,须要引入 Jar 包,让调用方好能依赖接口的定义做代理。
  • 畛域封装 – trigger:触发器层,个别也被叫做 adapter 适配器层。用于提供接口实现、音讯接管、工作执行等。所以对于这样的操作,这里把它叫做触发器层。
  • 畛域编排【可选】– case:畛域编排层,个别对于较大且简单的的我的项目,为了更好的防腐和提供通用的服务,个别会增加 case/application 层,用于对 domain 畛域的逻辑进行封装组合解决。但对于一些小我的项目来说,齐全能够去掉这一层。大量一层对象转换,代码的保护老本会升高很多。
  • 畛域封装 – domain:畛域模型服务,是一个十分重要的模块。无论怎么做 DDD 的分层架构,domain 都是必定存在的。在一层中会有一个个细分的畛域服务,在每个服务包中会有【模型、仓库、服务】这样 3 局部。
  • 仓储服务 – infrastructure:根底层依赖于 domain 畛域层,因为在 domain 层定义了仓储接口须要在根底层实现。这是依赖倒置的一种设计形式。所有的仓储、接口、事件音讯,都能够通过依赖倒置的形式进行调用。
  • 类型定义 – gateway:对于内部接口的调用,也能够从基础设施层拆散一个专门的 gateway 网关层,来封装内部 RPC/HTTP 等类型接口的调用。
  • 类型定义 – types:通用类型定义层,在咱们的零碎开发中,会有很多类型的定义,包含;根本的 Response、Constants 和枚举。它会被其余的层进行援用应用。(这一层没有画到图中)

综上就是 DDD 架构思维下的工程分层模型构造,DDD 架构的畛域驱动设计的重点包含;构造边界更加清晰、器重上下文调用、拆散业务性能与根底撑持。总之一句话,就是各司其职。那么鉴于如此清晰工程构造,该如何将旧存工程,MVC 转向 DDD 呢?接下来就重点介绍下。

三、工程重构(MVC->DDD)

通过实际验证,不须要太高老本,MVC 就能够人造的向 DDD 工程分层的模型构造转变。重点是不扭转原有的工程模块的依赖关系,将贫血的 domain 对象层,设计为充血的构造。对于 domain 本来在 MVC 分层构造中,就是一个被依赖层,恰好能够与其余层做依赖倒置的设计方案解决。具体如图所示;

左侧是咱们常见的 MVC 分层构造,右侧是给大家上文解说过的 DDD 分层构造。从 MVC 到 DDD 的映射,应用了雷同色彩进行标注。之后我来介绍一些细节;

在 MVC 分层构造中,所有的逻辑都集中在 service 层,也是文中提到的腐化最重大的层,要治理的也是这一层。所以首先咱们要将 service 里的性能进行拆解。

  1. service 中具备畛域个性的服务实现,抽离到本来贫血模型的 domain 中。在 domain 分层中增加 xxx、yyy、zzz 分层畛域包,别离实现不同性能。留神每个分层畛域包内都具备残缺的 DDD 畛域服务内所需的模块
  2. service 中的根底性能组件,如;缓存 Redis、配置核心等,迁徙到 dao 层。这里咱们把 dao 层看做为基础设施层。它与 domain 畛域层的调用关系,为依赖倒置。也就是 domain 层定义接口,dao 层依赖于 domain 定义的接口,做依赖倒置实现接口。
  3. service 自身最初被当做 application/case 层,来调用 domain 层做服务的编排解决。

因为恰好,MVC 分层构造中,也是 service 和 dao 依赖于 domain,这和 DDD 分层构造是统一的。所以通过这样的映射拆分代码实现调用构造后,并不会让工程构造发生变化。那么只有工程构造不发生变化,咱们的革新老本就只剩下代码编写格调和旧代码迁徙老本。

MVC 分层构造中的 export 层是 RPC 接口定义层,由 web 层实现。web 是对 service 的调用。也就是 DDD 分层构造中调用 application 编排好的服务。这部分无需改变。但如果你原有工程把 domain 也暴漏出去了,则须要把对应的包迁徙到 export 因为 domain 包有太多的外围对象和属性,还包含数据库长久化对象。这些都不应该被暴漏。

MVC 分层中,因为有须要对外部 RPC 接口的调用,所以会独自有一层 RPC 来封装其余服务的接口。这一层被 domain 畛域应用层,能够定义 adapter 适配器接口,通过依赖倒置,在 rpc 层实现 domain 层定义的调用接口。

此外 dao 层,在 MVC 构造中本来是比拟繁多的。但通过革新后会须要把根底的 Redis 应用、配置中应用,都迁徙到 dao 层。因为本来在 service 层的话,domain 层是调用不到的这些根底服务的,而且也不合乎服务性能边界的划分。

综上,就是从 MVC 到 DDD 重构架构的拆解实现计划。这是一种最低老本的最佳施行策略,齐全能够保障 MVC 的构造,又能够利用上 DDD 的架构分层劣势。也能使用 DDD 畛域驱动设计思维,重构旧代码,减少可维护性。

到这里,分层构造问题咱们说分明了。从 MVC 调整结构到 DDD 后,工程模型中的调用链路关系是什么样呢?接下来咱们在开展架构,看细节关系。

四、分层调用链路

接下来咱们把 DDD 的分层架构平铺开展,看看从一个接口的实现到各个模块分层中的调用链路关系是什么样的。这样在做本人的代码开发中也能够参考到应该把什么的性能调配到哪个模块中解决。

从 APP 层、触发器层、应用层,这三块次要对畛域层的上下文逻辑封装、触发式 (MQ、HTTP、JOB) 应用,并最终在应用层中打包公布上线。这一部分的都是应用的解决,所以也不会有太简单的操作。

当进入畛域层开始,也是智力集中体现的开始了。所有你对工程的形象能力,都在这一块区域体现。

接下来咱们着种介绍下畛域层和根底层的模块职责性能;图中下方是对象的流转,能够留神下。

1. 畛域服务层

咱们能够当 domain 畛域层为一个充血模型构造,在一个 domain 畛域层中,能够有多个畛域包。当然现实状态下,如果你的 DDD 拆分的特地洁净的新工程,那么可能一个 domain 就一个畛域。但大部分时候微服务的拆分鉴于老本思考不会那么细,还有一些老工程的重构,都是一个工程内有多个畛域,对应的解决方案是在一个工程下建多个同级分层包。比方;账户畛域包、授信畛域包、结算畛域包等,每个包内聚合实现不同的性能。

每一个 domain 下的畛域包内,都包含;model 模型、仓储、接口、事件和服务的解决。

model 模型对象;

  • aggreate:聚合对象,实体对象、值对象的协同组织,就是聚合对象。
  • entity:实体对象,大多数状况下,实体对象 (Entity) 与数据库长久化对象 (PO) 是 1v1 的关系,但也有为了封装一些属性信息,会呈现 1vn 的关系。
  • valobj:值对象,通过对象属性值来辨认的对象 By《实现畛域驱动设计》

repository 仓储服务;从数据库等数据源中获取数据,传递的对象能够是聚合对象、实体对象,返回的后果能够是;实体对象、值对象。因为仓储服务是由根底层(infrastructure) 援用畛域层(domain),是一种依赖倒置的构造,但它能够人造的隔离 PO 数据库长久化对象被援用。

adapter 接口服务;是依赖于外包的其余 HTTP/RPC 接口的封装调用,通过在 domain 畛域层定义适配器接口,再有依赖于 domain 的根底层设施层或者一个独自的专门解决接口的额定分层,来实现 domain 定义的适配器接口,实现对依赖的 HTTP/RPC 进行封装解决。

event 事件音讯;在服务实现中,进行会有业务实现后,对外发送音讯的状况。这个时候,能够在畛域模型中定义事件音讯的接口,再有基础设施层实现音讯的推送。

service 服务设计;这里要留神,不要以定义了聚合对象,就把超过 1 个对象以外的逻辑,都封装到聚合中,这会让你的代码前期越来越难保护。聚合更应该重视的是和本对象相干的繁多简略封装场景,而把一些重外围业务方到 service 里实现。此外;如果你的设计模式利用不佳,那么无论是畛域驱动设计、测试驱动设计还是换了三层和四层架构,你的工程质量仍然会十分差。

2. 基础设施层

提供数据库长久化 提供 Redis 和配置核心数据撑持 提供事件音讯推送 提供内部服务接口封装。总之这一层的外围目标就是更好的辅助 domain 畛域层实现畛域性能的开发。

而调用形式则为依赖倒置,也就是 畛域服务层 定义接口,基础设施层 做性能实现。这样能够无效的防止根底基础设施层中的对象被对外暴漏,如数据库长久化对象,在这样的分层构造中,人造的被爱护在根底设置层中,内部是没法引入的,否则就循环依赖了。

有了这一层当前,domain 层不会关系数据的细节解决。传递给基础设施层的办法中,会把聚合对象或实体对象通过接口办法传递下来。之后在基础设施层中实现数据事务的操作。也会含有事务处理后,写入 Redis 缓存和发送 MQ 音讯。如果说有夸畛域的事务,个别可能就是跨库表,这个时候要应用 MQ 事件的形式进行驱动。

3. 类型对象层

这一层就比较简单了,只是一些通用的出入参对象 Response,还有枚举对象、异样对象等。供应于对外的接口层应用。但如果是 RPC 这样的接口,倡议同 RPC 对外提供的接口形容包中提供,因为对外只提供 1 个轻量化的包且不依赖于任何其余包,是最好保护治理的。

五、只是换了别墅

从 MVC 到 DDD,咱们有一点是必须分明的认知的。

从 MVC 到 DDD 咱们只是换了一个更大、格局更清晰的房子🏡,但并不能决定你从 MVC 到 DDD 代码就变得十分洁净、丑陋、整洁了。因为从 MVC 到 DDD 只是骨架变了,但骨架之下的血肉并没有扭转。

如果你仍是把原有的烂代码平移到新的分层架构中,就相当于把老房子里的破旧家具衣物鞋帽搬过去而已。所以按照与软件设计的准则;分治、形象和常识,中的常识是设计准则和设计模式的使用。所以要想把代码写好,就肯定是要把DDD + 设计模式,能力真的把代码写好。接下来,小傅哥再给大家举个应用模式在 DDD 分层构造中重构的案例。

六、重构现有代码

软件设计第一准则,康威定律所提到的,分治、形象和常识,是用于零碎设计和实现的领导阐明。分治和形象,咱们能够用 DDD 思维映射的分层架构来解决,但常识则是设计准则和设计模式的使用。

所以,如果没有正当的使用设计常识来对代码进行细化解决,那么即便拆分出流程边界在清晰的架构,也很难做出好保护的代码。而通常最罕用的设计模式,无外乎;工厂、策略、模板的组合应用,少部分会用到责任链、建造者、组合模式。那么接下来,在分享一个带有流程的设计模式应用,让大家能够有一份可参考的工程代码设计。

1. 场景设定

这里咱们做一个提额场景的设定。预计大家都用过信用卡💳,它有一个初始的额度,在后续的应用中会随着信用的积攒和生产的减少,进行进步额度。而额度的进步则须要一系列的校验判断并最终做出提额解决。流程如下;

这样的流程图,是咱们做业务开发的小伙伴,常常看到的。做一系列的流程判断解决,之后实现一个具体的性能。简略来说,就是 if···else 写代码,一条条的校验。但写着写着,工夫一长就会发现代码变得特地凌乱。最次要的起因就是,那些为了撑持实现业务的各类判断是不稳固因素,会随着业务的变动一直的调整。甚至有时候就间接下掉了。但你的代码就中多就了一条 // 业务说临时不应用,你也不敢删!就像有首歌唱的🎤:“需要仍旧停在旷野上,你的代码被越拉越长。直到远去的马蹄声响,召唤你的 Bug 传四方。”

所以对于这样的性能流程设计,怎么办呢?总不能让旷野的马蹄,始终拉着你的 bug 在奔袭。

2. 代码现状

一个接口一个实现,一个实现代码一片。` ` 一片一片,又一片,代码行数,两三千。

大部分咱们在 MVC 工程分层构造下,参加开发的代码,根本都是定义一个接口,就写一片性能实现。性能实现中,如果看到有现成的接口,间接拿来复用。所有的实现并不会基于接口、形象、模板等进行,所以最终这样的代码腐化的十分重大。

3. 从新分层

重构前,先阐明下新的分层解决;如图

  • 首先,在原有的 domain 贫血模型中,增加一个对应的畛域包。credit 你能够是本人的其余的畛域包。之后的 domain 则为充血模型设计。
  • 之后,在畛域包内实现本人的业务逻辑,留神这里须要用到设计模式来实现。代码实现中须要用到的数据查问、缓存应用、接口调用,全副采纳依赖倒置的形式让根底层 / 接口层,来提供具体的实现逻辑。而 domain 层只是定义接口和应用 Spring 的注入进行应用。

4. 重构代码

抽象类,是一个十分好用的类。一种是能够定义出流程构造,让代码变得清晰洁净。再有一种是定义共用办法,让其余实现类可复用。

那么这里,咱们就应用抽象类定义模板 + 策略和工厂实现的规定引擎解决频繁变动的校验类流程,实现代码开发。如图咱们先设计下代码的实现构造。

  • 首先,定义一个受理调额的接口。因为额度的调整,包含;提额、降额。所以不要把名字写的太死。
  • 之后,由抽象类实现接口。在抽象类中定义出整个调用链路关系,并把一些专用的数据类撑持逻辑,提到撑持类里。这和 Spring 的设计很像。
  • 之后,因为规定校验这货色是为了撑持外围流程走上来的,而且还是随着业务频繁变动的。那就没必要在主线业务流程中,用 if···else 贴膏药的写代码,而是应该拆解进去。所以这里设计一个策略模式实现的规定校验,并通过工厂对外提供服务。
  • 最初,这些货色整机类的货色都解决好后。就能够在抽象类的子类实现中进行调用解决了。

5. 代码出现

通过设计模式的重构解决,当初的代码就以如下模式体现了。—— 拆解进去的伪代码,具体能够参考过往的一些设计模式使用。

public AdjustAssetOrderEntity acceptAdjustAssetApply(AdjustAssetApplyEntity adjustAssetApplyEntity) {
    // 1. 参数校验
    this.parameterVerification(adjustAssetApplyEntity);
  
    // 2. 查问申请单数据,如曾经存在则间接返回
    AdjustAssetOrderEntity orderEntity = queryAssetLog(adjustAssetApplyEntity.getPin(), adjustAssetApplyEntity.getAccountType(), adjustAssetApplyEntity.getTaskNo(), adjustAssetApplyEntity.getAdjustType());
    if (null != orderEntity) {log.info("pin={} taskNo={} 受理申请,检索到工作存在进行中的申请单。", adjustAssetApplyEntity.getUserId(), adjustAssetApplyEntity.getTaskNo());
        return orderEntity;
    }
  
    // 3. 以下流程放到分布式锁内解决【防止雷同申请二次进入】String lockId = genLockId(adjustAssetApplyEntity.getAdjustType(), adjustAssetApplyEntity.getUserId());
    try {
        // 3.1 分布式锁:加锁
        long state = lock(lockId);
        if (0 == state) {throw new AccountRuntimeException(BizResultCodeEm.DISTRIBUTED_LOCK_EXCEPTION.getCode(), "分布式锁异样,以后用户行为解决中。");
        }
      
        // 3.2 账户查问
        UserAccountInfoDTO userAccountInfoDTO = queryJtAccount(adjustAssetApplyEntity.getUserId(), adjustAssetApplyEntity.getAccountType());
      
        // 3.3 根底校验;(1)账户类型、(2)状态状态、(3)额度类型、(4)账户逾期、(5)费率类型【暂无】LogicCheckResultEntity logicCheckResultEntity = doCheckLogic(adjustAssetApplyEntity, userAccountInfoDTO,
                DefaultLogicFactory.LogicModel.ACCOUNT_TYPE_FILTER.getCode(),
                DefaultLogicFactory.LogicModel.ACCOUNT_STATUS_FILTER.getCode(),
                DefaultLogicFactory.LogicModel.ACCOUNT_QUOTA_FILTER.getCode(),
                DefaultLogicFactory.LogicModel.ACCOUNT_OVERDUE_FILTER.getCode());
      
        if (!AssetCycleQuotaAlterCodeEnum.E0000.getCode().equals(logicCheckResultEntity.getCode())) {log.info("userId={} taskNo={} 规定校验过滤拦挡。code:{} info:{}", adjustAssetApplyEntity.getUserId(), adjustAssetApplyEntity.getTaskNo(), logicCheckResultEntity.getCode(), logicCheckResultEntity.getInfo());
            throw new AccountRuntimeException(logicCheckResultEntity.getCode(), logicCheckResultEntity.getInfo());
        }
      
        // 3.4 受理调额
        return this.acceptAsset(adjustAssetApplyEntity, userAccountInfoDTO);
    } finally {
        // 3.1 分布式锁:解锁
        this.unlock(lockId);
    }
}

这样的解决后,代码就变得十分清晰了。

  1. 先是做根底的校验和数据的查问判断,之后加锁防止一个人超时申请。而后,进行规定引擎的调用和解决,依据不同的诉求,开发不同的规定,并配置的形式进行应用。
  2. 最初所有的这些货色解决实现后,就是做最终的调额解决了。

七、实战学习

  • 重构,是始终都在产生的事件,不能积攒到最初才重构。那只有重做的可能。
  • 工厂、模板、策略,这 3 个设计模式,就能够解决 80% 的场景问题。
  • 小傅哥的编码标准也会成为搭档参考的案例,所以小傅哥会更严格要求本人的规范。

留神📢,很多学不会 DDD,也学不会设计模式的。凭良心说,不就是没看见好的代码,没跟着有价值的我的项目走一遍吗!

正文完
 0