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

一.价值观的碰撞

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

二、从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等组件。
好了,稀里糊涂的讲了一大堆,有不对的地方请多多指教,只是在学习过程中的一点碎碎念,也算是给久未更新的博客扫扫土,最近的确是有点懈怠了。

你可能感兴趣的:(.net,微服务)