从MVC到DDD转变过程中的一点碎碎念

2023-10-30 21:50

本文主要是介绍从MVC到DDD转变过程中的一点碎碎念,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一.价值观的碰撞

最近再看《三体》电视剧,开篇就演很多科学界的大V,叫嚣着“物理学不存在了”,然后自杀。。。
从近期的经历而言,由于长期基于MVC架构的设计模式开发软件,突然转到基于DDD的设计模式时,会发现原来自己习以为常的一些编程方法,思维模式几乎都变样了。原来我坚持了多年的编码习惯,到了新的领域,一下子都成了错的了。
这就引发了一个不大不小的问题,系统重构的时候,就不只是重构了!如果长期使用贫血模型进行业务开发,那么我们在和产品或者任何需求方沟通的时候,或多或少会在写代码的时候加上一层自己的理解。
其实不只是这一层,甚至可以细化到软件开发的各个环节,比如用户需求经过产品经理的理解,转化成了产品需求,产品和研发沟通后,研发又把产品需求加上自己的理解转化成开发需求,开发过程中,设计数据库,写代码,等环节又会根据框架结构,再加一层开发自己的理解。。。
在这里插入图片描述
当初始的需求,经过多层转化后,实际的业务开发,由于掺杂了开发者的主观意志,难以避免的会造成诸如考虑不全面,数据库模型调整等问题,想起那种网上流行段子👇。
在这里插入图片描述
而当大量的逻辑补充堆积到了代码里,会使得项目变得越来越难以维护,慢慢发展成巨石项目,一旦到了这个阶段,无论是开发还是运营,大家对项目的期待标准都会降低,基本就是“能跑就行”,这也差不多就是系统需要重新出发的信号,要尽早规划布局,未雨绸缪,否则大厦将倾,可能就不是空话了。

二、从CURD到CQRS

在MVC的时代,管理数据的方式,一般是在单独的仓储层编写底层的数据库交互方法,然后上层编写业务逻辑,再通过构造函数完成接口注入,最后再到controller里完成调用,把最终的执行结果返回到用户的界面,这中间,根据业务不同,可能还涉及日志,缓存等一些逻辑,而实现方法也基本都是通过“调用”接口方法来完成。
这就产生了一个问题,就是各个层级之间相互依赖耦合,如果项目本身不大,那这其实不算问题,而一旦业务规模增长,这个麻烦就越来越大,代码也就开始有味道了。。
比如下面的代码,我要根据学生成绩,生成证书,伪代码的逻辑差不多就是这样

[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> GenerationCert([FromService]Tool1 tool1,[FromService] Tool2 tool2,[FromBody] CertModel model)
{var ret1 = await tool1.GenerationCertAsync(model);if(ret1.msg != "success"){return BadRequest();}    var ret2 = tool2.SendResultToStudent(ret1.Result);...//更多逻辑return SuccessfulRequest();
}

先根据模型结果,判定是否可以生成证书,生成之后还要通知学生,另外可能还要记录操作过程,传输日志等等,就需要一个个堆叠接口方法。虽然通过DI的方式,让容器管理来对象的生命周期,而要想完成业务,仍然避免不了一个个的添加依赖。
这大概就是基于MVC框架的CURD模式的一种落地实践,有优点,也有缺点,在云原生时代来临以前,是利大于弊的,因为这种模式小巧,轻便,更贴合人类大脑理解事物的思维方式。而缺点也是显而易见的,上不了量,一旦量上来,先不说各种并发问题,就是改起代码来,那复杂程度都是指数级上升的,注意,这里说的是复杂程度,不是难度!因为随着业务的规模变大,层级也会越分越多,服务边界越来越模糊,到处都是mvc层,甚至mc层,也就是会变得越来越没技术含量,越来越像工人拧螺丝,这不是我们追求的结果。
而CQRS的模式则是通过读写分离的设计模式,通过发布订阅的方式来完成层级间的通信,不用再一个方法里在调用另一个方法了,而是通过发布命令,订阅者接收命令来完成多个业务逻辑的整合,再配合EventBus或者一些其他的事件总线组件,完成事务一致性,这就解决了MVC模式里因为多重依赖造成的耦合性难题。
如果小伙伴了解过DDD,了解过CQRS,可能觉得我在说废话,但如果不了解,可能还是懵逼的,因为我就是刚从那个阶段过来,而且也还仅仅是开了一点点窍,就忍不住来这里大放厥词了,哈哈,因为真的是有那种柳暗花明又一村的感觉,迫不及待的要分享。
实现CQRS的常用方式就是使用事件总线(EventBus),这个在各类微服务开发框架里应该都算是基础设施了,伪代码如下
比如我在领域层(Domain)定义了数据库实体(Entity)

public class CourseSection : FullAggregateRoot<long, int>
{public string Caption { get; set; } = null!;public string SubCaption { get; set; } = null!;public string Description { get; set; } = null!;public int OrderNum { get; set; }private Guid _courseInfoId { get; set; }public CourseInfo CourseInfo { get; private set; }=null!;}

,定义了仓储接口(IRepositories)

public interface ICourseSectionRepository : IRepository<CourseSection, long>
{IQueryable<CourseSection> Query(Expression<Func<CourseSection, bool>> predicate);
}

在基础设施层(Infrastructure)定义了仓储实现,并继承仓储接口

public class CourseSectionRepository : Repository<CourseDbContext, CourseSection, long>, ICourseSectionRepository
{private readonly CourseDbContext _context;public CourseSectionRepository(CourseDbContext context, IUnitOfWork unitOfWork) : base(context, unitOfWork){_context = context;}public IQueryable<CourseSection> Query(Expression<Func<CourseSection, bool>> predicate){return _context.Set<CourseSection>().Where(predicate);}
}

之后,又在用户接口层或者应用层(Application)定义Query和订阅事件处理的Handler

public record CourseSectionsQuery : ItemsQueryBase<PaginatedResultDto<CourseSectionItemDto>>
{public string? Caption { get; set; }public override PaginatedResultDto<CourseSectionItemDto> Result { get; set; } = null!;}
public class CourseSectionHandler
{private readonly ICourseSectionRepository _courseSectionRepository;public CourseSectionHandler(ICourseSectionRepository courseSectionRepository){_courseSectionRepository = courseSectionRepository;}[EventHandler]public async Task GetListAsync(CourseSectionsQuery query){Expression<Func<CourseSection, bool>> exp = item => true;exp = exp.And(!query.Caption.IsNullOrWhiteSpace(), courseInfo => courseInfo.Caption.Contains(query.Caption!));var queryable = _courseSectionRepository.Query(exp);var total = await queryable.LongCountAsync();var totalPages = (int)Math.Ceiling((double)total / query.PageSize);var result = await queryable.Include(item => item.CourseInfo).OrderByDescending(item => item.CreationTime).Skip((query.Page - 1) * query.PageSize).Take(query.PageSize).OrderByDescending(ci => ci.Id).ToListAsync();query.Result = new PaginatedResultDto<CourseSectionItemDto>(total, totalPages, result.Map<List<CourseSectionItemDto>>());}
}

最后,要在服务层发布事件,然后把最终的结果放到DTO里,最终返回给用户。

public class CourseSectionService : ServiceBase
{        public async Task<PaginatedResultDto<CourseSectionItemDto>> GetListAsync(IEventBus eventBus,CancellationToken cancellationToken,string? caption = null,int page = 1,int pageSize = 10){var query = new CourseSectionsQuery(){Caption = caption,Page = page,PageSize = pageSize};await eventBus.PublishAsync(query, cancellationToken);return query.Result;}
}

定义DTO我就不贴代码了,不具备典型意义。
到此,这个例子基本算结束了,回头看,我们为了完成一次查询操作,穿插了基础设施层,领域层,服务层,用户接口层,但实际动作的完成是由服务层发起,在接口层完成,领域层和基础设施层只是提供了数据支撑,而整个过程,没有产生依赖关系,消息传输的介质是DTO,提供传输服务的是EventBus,同样的,如果是写操作,我们就需要把Query定义该成类似的Command,并完成对应的写入流程就好,看起来好像是多做了很多工作,但实际上,即便是你的业务复杂程度多了以后,要做的也差不多就只是这些,所以基于DDD模式实现的CQRS是典型的后期型选手,前期上手可能觉得困难麻烦,但随着业务规模的增长,优势就会慢慢发挥出来,尤其适用于微服务领域。
这就是CQRS落地的一种最简单的实例了。
当然实现CQRS模式,是可以依赖开发框架的,dotnet领域有ABP,Masa.framework这种全包型的开发框架本身就支持,而如果是自己集成,则可以依赖CAP,MassTransit,MeidatR等组件。
好了,稀里糊涂的讲了一大堆,有不对的地方请多多指教,只是在学习过程中的一点碎碎念,也算是给久未更新的博客扫扫土,最近的确是有点懈怠了。

这篇关于从MVC到DDD转变过程中的一点碎碎念的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/310810

相关文章

Spring Security注解方式权限控制过程

《SpringSecurity注解方式权限控制过程》:本文主要介绍SpringSecurity注解方式权限控制过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、摘要二、实现步骤2.1 在配置类中添加权限注解的支持2.2 创建Controller类2.3 Us

Spring MVC跨域问题及解决

《SpringMVC跨域问题及解决》:本文主要介绍SpringMVC跨域问题及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录跨域问题不同的域同源策略解决方法1.CORS2.jsONP3.局部解决方案4.全局解决方法总结跨域问题不同的域协议、域名、端口

Spring AI集成DeepSeek三步搞定Java智能应用的详细过程

《SpringAI集成DeepSeek三步搞定Java智能应用的详细过程》本文介绍了如何使用SpringAI集成DeepSeek,一个国内顶尖的多模态大模型,SpringAI提供了一套统一的接口,简... 目录DeepSeek 介绍Spring AI 是什么?Spring AI 的主要功能包括1、环境准备2

SpringBoot集成图片验证码框架easy-captcha的详细过程

《SpringBoot集成图片验证码框架easy-captcha的详细过程》本文介绍了如何将Easy-Captcha框架集成到SpringBoot项目中,实现图片验证码功能,Easy-Captcha是... 目录SpringBoot集成图片验证码框架easy-captcha一、引言二、依赖三、代码1. Ea

pycharm远程连接服务器运行pytorch的过程详解

《pycharm远程连接服务器运行pytorch的过程详解》:本文主要介绍在Linux环境下使用Anaconda管理不同版本的Python环境,并通过PyCharm远程连接服务器来运行PyTorc... 目录linux部署pytorch背景介绍Anaconda安装Linux安装pytorch虚拟环境安装cu

SpringBoot项目注入 traceId 追踪整个请求的日志链路(过程详解)

《SpringBoot项目注入traceId追踪整个请求的日志链路(过程详解)》本文介绍了如何在单体SpringBoot项目中通过手动实现过滤器或拦截器来注入traceId,以追踪整个请求的日志链... SpringBoot项目注入 traceId 来追踪整个请求的日志链路,有了 traceId, 我们在排

Spring Boot 3 整合 Spring Cloud Gateway实践过程

《SpringBoot3整合SpringCloudGateway实践过程》本文介绍了如何使用SpringCloudAlibaba2023.0.0.0版本构建一个微服务网关,包括统一路由、限... 目录引子为什么需要微服务网关实践1.统一路由2.限流防刷3.登录鉴权小结引子当前微服务架构已成为中大型系统的标

Java中对象的创建和销毁过程详析

《Java中对象的创建和销毁过程详析》:本文主要介绍Java中对象的创建和销毁过程,对象的创建过程包括类加载检查、内存分配、初始化零值内存、设置对象头和执行init方法,对象的销毁过程由垃圾回收机... 目录前言对象的创建过程1. 类加载检查2China编程. 分配内存3. 初始化零值4. 设置对象头5. 执行

SpringBoot整合easy-es的详细过程

《SpringBoot整合easy-es的详细过程》本文介绍了EasyES,一个基于Elasticsearch的ORM框架,旨在简化开发流程并提高效率,EasyES支持SpringBoot框架,并提供... 目录一、easy-es简介二、实现基于Spring Boot框架的应用程序代码1.添加相关依赖2.添

SpringBoot中整合RabbitMQ(测试+部署上线最新完整)的过程

《SpringBoot中整合RabbitMQ(测试+部署上线最新完整)的过程》本文详细介绍了如何在虚拟机和宝塔面板中安装RabbitMQ,并使用Java代码实现消息的发送和接收,通过异步通讯,可以优化... 目录一、RabbitMQ安装二、启动RabbitMQ三、javascript编写Java代码1、引入