写点什么

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

作者:为自己带盐
  • 2023-02-16
    河北
  • 本文字数:3818 字

    阅读完需:约 13 分钟

从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!; }
复制代码


,定义了仓储接口

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,提供数据服务的是 ORM,同样的,如果是写操作,我们就需要把 Query 定义该成类似的 Command,并完成对应的写入流程就好,看起来好像是多做了很多工作,但实际上,即便是你的业务复杂程度多了以后,要做的也只是这些,属于是典型的后期选手,前期上手可能觉得困难麻烦,但随着业务规模的增长,优势就会慢慢发挥出来,尤其适用于微服务领域。

这就是 CQRS 落地的一种最简单的实例了。当然实现 CQRS 模式,是可以依赖开发框架的,dotnet 领域有 ABP,Masa.Framework 这种全包型的开发框架本身就支持,而如果是自己集成,则可以依赖CAPMassTransitMeidatR等组件自己封装。(个人建议,新手先依赖框架能力,后期再 DIY)

好了,稀里糊涂的讲了一大堆,有不对的地方请多多指教,只是在学习过程中的一点碎碎念,也算是给久未更新的博客扫扫土,最近的确是有点懈怠了。

发布于: 2023-02-16阅读数: 46
用户头像

学着码代码,学着码人生。 2019-04-11 加入

努力狂奔的小码农

评论 (2 条评论)

发布
用户头像
推荐了
18 小时前 · 上海
回复
谢了老铁~
6 小时前 · 河北
回复
没有更多了
从MVC到DDD转变过程中的一点碎碎念_DDD_为自己带盐_InfoQ写作社区