一、价值观的碰撞
最近再看《三体》电视剧,开篇就演很多科学界的大 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 这种全包型的开发框架本身就支持,而如果是自己集成,则可以依赖CAP,MassTransit,MeidatR等组件自己封装。(个人建议,新手先依赖框架能力,后期再 DIY)
好了,稀里糊涂的讲了一大堆,有不对的地方请多多指教,只是在学习过程中的一点碎碎念,也算是给久未更新的博客扫扫土,最近的确是有点懈怠了。
评论 (2 条评论)