哈喽你们好哟,今天又到了老张的周二四放送时间了,固然中间还有不按期的更新(由于我的看papi酱看多了),这个主要是针对小伙伴提出的问题和优秀解决方案而写的,通过上周两篇DDD领域驱动设计的试水,我发现一个问题,这个DDD的水是真的深啊~或者来讲就是这个思想的转变是不舒服的,好多小伙伴就说有点儿转不过来,固然我也是,一直站在原地追着影子跑,固然这个系列我会一直坚持下去的,你们若是感受我写的没有误人子弟或者感受看着还有点儿意思,请不要着急,多多评论,我虽然没有更新,可是也一直在线,提出来的问题能够一块儿讨论,周末的时候,我又和“李大爷”一块儿从非专业的角度,从领域专家的角度思考了下DDD领域驱动设计的思想,感受还有点儿领悟的,这里给你们分享下,若是你如今还对为何使用DDD,或者还有DDD就像是一个三层架构或者MVC架构的想法的话,看完这一篇应该就能稍微的明白了。html
很开森的是上周的问题你们评论很好,也上了24小时评论榜单,但愿你们均可以多评论评论😀,真的很精彩,你们能够再去看看《二 ║ DDD入门 & 项目结构粗搭建》,评论席的内容的含金量,甚至都超过了个人正文内容,并且也能知足老张小小的虚荣感,今天呢,我就先在本文的上半篇重点说一下你们最最最热心的两个问题,而后再继续推动我们的项目代码,就主要从如下三个大块铺开来讲:git
一、DDD的意义到底在哪里?为何很难理解?github
二、为何要使用仓储,EFCore不就是一个仓储么?算法
三、限界上下文如何定义呢?包含了平时遇到的哪些东西?数据库
悄悄说:通过周末的讨论,我发现上次我们新建的那个关于 Customer 领域对象很差举例子,问答程序提及来也不是很顺口,因此我已经修改为了 Student 模型,而后我也想到了一个领域——教务系统,这个你们必定是熟悉的不能再熟悉了,每一个小伙伴都是通过上学的噩梦里过来的(哈哈学霸就另说了),之后我们就用这个教务领域来展开说明,你们也能都在一条思路上,并且也不会花心思去考虑问答系统这个不熟悉的领域。编程
关于DDD的使用,网上已经有不少的栗子了,不管是各类粘贴复制的教科书,仍是自个人一些心得,基本已经说完了,不过我每次读的时候,内心都是有点儿抗拒,一直都没办法看懂,今天我就决定用另外一个办法,来和你们好好说一下这个DDD领域驱动设计的意义到底在哪里。这个时候请你本身先想想,若是使用DDD会有哪些好处,若是说看完了我写的,感受有共鸣,那很不错,要是感受我写的认为不对,欢迎评论席留下你的意见哟,开源嘛,不能让我本身发表见解的,也让个人博客能够多在你们的面前展示下哈哈。json
故事就从这里开始:我们有一个学校,就叫从壹大学(我瞎起的名字哈哈),咱们从壹大学要开发一套教务系统,这个系统涵盖了学校的方方面面,从德智体美劳都有,其中就有一个管理后台,任何人均可以登陆进去,学习查看本身的信息和成绩等,老师能够选择课程或者修改本身班级的学生的我的信息的,如今就说其中的一个小栗子 —— 班主任更改学生的手机号。咱们就用普通的写法,就是咱们平时在写或者如今在用的流程来设计这个小方法。api
请注意:当前系统就是一个 领域,里边会有不少 子领域,这个你们应该都能懂。缓存
这个方法逻辑很简单,就是把学生的手机号更新一下就行,平时我们必定是咣咣把数据库建好,而后新建实体类,而后就开始写这样的一批方法了,话很少说,直接看看怎么写(这是伪代码):安全
/// <summary> /// 后台修改学生手机号方法 /// </summary> /// <param name="NewPhoNumber"></param> /// <param name="StudentId"></param> /// <param name="TeacherId"></param> public void UpdateStudentPhone(string newPhoNumber,int studentId,int teacherId) { //核心1:连数据,获取学生信息,而后作修改,再保存数据库。 }
这个方法特别正确,并且是核心算法,简单来看,已经知足咱们的需求了,可是却不是完整的,为何呢,由于只要是管理系统涉及到的必定是有权限问题,而后咱们就很开始和DBA讨论增长权限功能。
请注意:这里说到的修改手机号的方法,就是咱们以后要说到的领域事件,学生就是咱们的领域模型,固然这里边还有聚合根,值对象等等,都从这些概念中提炼出来。
刚需就是指必须使用到的一些功能,是仅此于核心功能的下一等级,若是按照咱们以前的方法,咱们就很天然的修改了下咱们的方法。
故事:领导说,上边的方法好是好,可是必须增长一个功能强大的权限系统,不只能学生本身登陆修改,还能够老师,教务处等等多方修改,还不能冲突,嗯。
/// <summary> /// 后台修改学生手机号方法 /// </summary> /// <param name="NewPhoNumber"></param> /// <param name="StudentId"></param> /// <param name="TeacherId"></param> public void UpdateStudentPhone(string newPhoNumber,int studentId,int teacherId) { //重要2:首先要判断固然 Teacher 是否有权限(好比只有班主任能够修改本班) //注意这个时候已经把 Teacher 这个对象,给悄悄的引进来了。 //------------------------------------------------------------ //核心:连数据,获取学生信息,而后作修改,再保存数据库。 }
这个时候你必定会说咱们可使用JWT这种呀,固然你说的对,是由于我们上一个系列里说到这个了,这个也有设计思想在里边,今天我们就暂时先用平时我们用到的上边这个方法,集成到一块儿来讲明,只不过这个时候咱们发现咱们的的领域里,不只仅多了 Teacher 这个其余模型,并且还多了与主方法无关,或者说不是核心的事件。
这个时候,咱们在某些特定的方法里,已经完成权限,咱们很开心,而后交给学校验收,发现很好,而后就上线了,故事的第一篇就这么结束了,你会想,难道还有第二篇么,没错!事务老是源源不断的的进来的,请耐心往下看。
请注意:这个权限问题就是 切面AOP 编程问题,之前已经说到了,这个时候你能想到JWT,说明很不错了,固然还能够用Id4等。
这个不知道你是否能明白,这个说白了就是操做日志,固然你能够和错误日志呀,接口访问日志一块儿联想,我感受也是能够的,不过我更喜欢把它放在事件上,而不是日志这种数据上。
故事:通过一年的使用,系统安静平稳,没有bug,一切正常,可是有一天,学生小李本身换了一个手机号,而后就去系统修改,居然发现本身的我的信息已经被修改了(是班主任改的),小李很神奇这件事,而后就去查,固然是没有记录的,这个时候反馈给技术部门,领导结合着其余同窗的意见,决定增长一个痕迹历史记录页,将痕迹跟踪提上了日程。咱们就这么开发了。
/// <summary> /// 后台修改学生手机号方法 /// </summary> /// <param name="NewPhoNumber"></param> /// <param name="StudentId"></param> /// <param name="TeacherId"></param> public void UpdateStudentPhone(string newPhoNumber,int studentId,int teacherId) { //重要:首先要判断固然 Teacher 是否有权限(好比只有班主任能够修改本班) //注意这个时候已经把 Teacher 这个对象,给悄悄的引进来了。 //------------------------------------------------------------ //核心:连数据,或者学生信息,而后作修改,再保存数据库。 //------------------------------------------------------------ //协同3:痕迹跟踪(你能够叫操做日志),获取固然用户信息,和老师信息,连同更新先后的信息,一块儿保存到数据库,甚至是不一样的数据库地址。 //注意,这个是一个突发的,项目上线后的需求 }
这个时候你可能会说,这个项目太假了,不会发生这样的事情,这些问题都应该在项目开发的时候讨论出来,并解决掉,真的是这样的么,这样的事情多么常见呀,咱们平时开发的时候,就算是一个特别成熟的领域,也会在项目上线后,增长删除不少东西,这个只是一个个例,你们联想下平时的工做便可。
这个时候若是咱们还采用这个方法,你会发现要修改不少地方,若是说咱们只有几十个方法还行,咱们就粘贴复制十分钟就行,可是咱们项目有十几个用户故事,每个故事又有十几个到几十个不等的用例流,你想一想,若是咱们继续保持这个架构,咱们到底应该怎么开发,可能你会想到,还有权限管理的那个AOP思想,写一个切面,但是真的可行么,咱们如今不只仅要获取数据前和数据后两块,还有用户等信息,切面我感受是颇有困难的,固然你也好好思考思考。
这个时候你会发现,我们平时开发的普通的框架已经支撑不住了,或者是已经很困难了,一套系统改起来已通过去好久了,并且不必定都会修改正确,若是一个地方出错,当前方法就受影响,一致性更别说了,试想下,若是咱们开发一个在线答题系统,就由于记录下日志或者什么的,致使结果没有保存好,学生是会疯的。第二篇就这么结束了,也许你的耐心已经消磨一半了,也许咱们觉得一块儿安静的时候,第三个故事又开始了。
请注意:这个事件痕迹记录就涉及到了 事件驱动 和 事件源 相关问题,之后会说到。
故事:咱们从壹大学新换了一个PM,嗯,在数据安全性,原子性的同时,更注重你们信息的一致性 —— 任何人修改都须要给当前操做人,被操做人,管理员或者教务处发站内消息通知,这个时候你会崩溃到哭的。
/// <summary> /// 后台修改学生手机号方法 /// </summary> /// <param name="NewPhoNumber"></param> /// <param name="StudentId"></param> /// <param name="TeacherId"></param> public void UpdateStudentPhone(string newPhoNumber,int studentId,int teacherId) { //重要:首先要判断固然 Teacher 是否有权限(好比只有班主任能够修改本班) //注意这个时候已经把 Teacher 这个对象,给悄悄的引进来了。 //------------------------------------------------------------ //核心:连数据,或者学生信息,而后作修改,再保存数据库。 //------------------------------------------------------------ //协同:痕迹跟踪(你能够叫操做日志),获取固然用户信息,和老师信息,连同更新先后的信息,一块儿保存到数据库,甚至是不一样的数据库地址。 //注意,这个是一个突发的,项目上线后的需求 //------------------------------------------------------------ //协同4:消息通知,把消息同时发给指定的全部人。 }
这个时候我就不具体说了,相信都已经离职了吧,但是这种状况就是天天都在发生。
请注意:上边我们这个伪代码所写的,就是DDD的 通用领域语言,也能够叫 战略设计。
上边的这个问题不知道是否能让你了解下软件开发中的痛点在哪里,二十年前 Eric Evans 就发现了,并提出了领域驱动设计的思想,就是经过将一个领域进行划分红不一样的子领域,各个子领域之间经过限界上下文进行分隔,在每个限界上下文中,有领域模型,领域事件,聚合,值对象等等,各个上下文互不冲突,互有联系,保证内部的一致性,这些之后会说到。
若是你对上下文不是很明白,你能够暂时把它理解成子领域,领域的概念是从战略设计来讲的,上下文这些是从战术设计上来讲的。
具体的请参考个人上一篇文章《三 ║ 简单说说:领域、子域、限界上下文》
你也许会问,那咱们如何经过DDD领域驱动设计来写上边的修改手机号这个方法呢,这里简单画一下,只是说一个大概意思,切分领域之后,每个领域之间互不联系,有效的避免了牵一发而动全身的问题,并且咱们能够很方便进行扩展,自定义扩展上下文,固然若是你想在教学子领域下新增一个年级表,那就不用新建上下文了,直接在改学习上下文中操做便可,具体的代码如何实现,我们之后会慢慢说到。
总结:这个时候你经过上边的这个栗子,不知道你是否明白了,咱们为何要在大型的项目中,使用DDD领域设计,并配合这CQRS和事件驱动架构来搭建项目了,它所解决的就是咱们在上边的小故事中提到的随着业务的发展,困难值呈现指数增加的趋势了。
这里就简单的说两句为何一直要使用仓储,而不直接接通到 EFCore 上:
一、咱们驱动设计的核心是什么,就是最大化的解决项目中出现的痛点,上边的小故事就是一个栗子,随着技术的更新,面向接口开发同时也变的特别重要,不管是方便重构,仍是方便IoC,依赖注入等等,都须要一个仓储接口来实现这个目的。
二、仓储还有一个重要的特征就是分为仓储定义部分和仓储实现部分,在领域模型中咱们定义仓储的接口,而在基础设施层实现具体的仓储。
这样作的缘由是:因为仓储背后的实现都是在和数据库打交道,可是咱们又不但愿客户(如应用层)把重点放在如何从数据库获取数据的问题上,由于这样作会致使客户(应用层)代码很混乱,极可能会所以而忽略了领域模型的存在。因此咱们须要提供一个简单明了的接口,供客户使用,确保客户能以最简单的方式获取领域对象,从而可让它专心的不会被什么数据访问代码打扰的状况下协调领域对象完成业务逻辑。这种经过接口来隔离封装变化的作法其实很常见,咱们须要什么数据直接拿就好了,而不去管具体的操做逻辑。
三、因为客户面对的是抽象的接口并非具体的实现,因此咱们能够随时替换仓储的真实实现,这颇有助于咱们作单元测试。
总结:如今随着开发,愈来愈发现接口的好处,不只仅是一个持久化层须要一层接口,小到一个缓存类,或者日志类,咱们都须要一个接口的实现,就好比如今我就很喜欢用依赖注入的方式来开发,这样能够极大的减小依赖,还有增大代码的可读性。
限界上下文已经说的很明白了,是从战术技术上来解释说明战略中的领域概念,你想一下,咱们如何在代码中直接体现领域的概念?固然没办法,领域是一个经过语言,领域专家和技术人员都能看懂的一套逻辑,而代码中的上下文才是实实在在的经过技术来实现。
你们能够在回头看看上边的那个故事栗子,下边都一个“请注意”三个字,里边就是咱们上下文中所包含的部份内容,其实限界上下文并无想象中的那么复杂,咱们只须要理解成是一个虚拟的边界,把不属于这个子领域的内容踢出去,对外解耦,可是内部经过聚合的。
用于咱们的特定的数据库链接,固然咱们能够公用 api 层的配置文件,这里单独拿出来,用于配合着下边的EFCore,进行注册。
{ "ConnectionStrings": { "DefaultConnection": "server=.;uid=sa;pwd=123;database=EDU" }, "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } }
在Christ3D.Infrastruct.Data 基础设施数据层新建 Context 文件夹,之后在基础设施层的上下文都在这里新建,好比事件存储上下文(上文中存储事件痕迹的子领域),
而后新建教务领域中的核心子领域——学习领域上下文,StudyContext.cs,这个时候你就不用问我,为啥在教务系统领域中,学习领域是核心子领域了吧。
/// <summary> /// 定义核心子领域——学习上下文 /// </summary> public class StudyContext : DbContext { public DbSet<Student> Students { get; set; } /// <summary> /// 重写自定义Map配置 /// </summary> /// <param name="modelBuilder"></param> protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new StudentMap()); base.OnModelCreating(modelBuilder); } /// <summary> /// 重写链接数据库 /// </summary> /// <param name="optionsBuilder"></param> protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // 从 appsetting.json 中获取配置信息 var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); // 定义要使用的数据库 optionsBuilder.UseSqlServer(config.GetConnectionString("DefaultConnection")); } }
在这个上下文中,有领域模型 Student ,还有之后说到的聚合,领域事件(上文中的修改手机号)等。
之后你们在迁移数据库的时候,可能会遇到问题,由于本项目有两个上下文,你们能够指定其中的操做
这里边有三个 Nuget 包,
Microsoft.EntityFrameworkCore//EFCore核心包 Microsoft.EntityFrameworkCore.SqlServer//EFCore的SqlServer辅助包 Microsoft.Extensions.Configuration.FileExtensions//appsetting文件扩展包 Microsoft.Extensions.Configuration.Json//appsetting 数据json读取包
这里给你们说下,若是你不想经过nuget管理器来引入,由于比较麻烦,你能够直接对项目工程文件 Christ3D.Infrastruct.Data.csproj 进行编辑 ,保存好后,项目就直接引用了
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp2.1</TargetFramework> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\Christ3D.Domain\Christ3D.Domain.csproj" /> </ItemGroup> //就是下边这一块 <ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.2.0-preview3-35497" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.2.0-preview3-35497" /> <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.2.0-preview3-35497" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0-preview3-35497" /> </ItemGroup> //就是上边这些 </Project>
Christ3D.Infrastruct.Data 基础设施数据层新建 Mappings 文件夹,之后在基础设施层的map文件都在这里创建,
而后新建学生实体map,StudentMap.cs
/// <summary> /// 学生map类 /// </summary> public class StudentMap : IEntityTypeConfiguration<Student> { /// <summary> /// 实体属性配置 /// </summary> /// <param name="builder"></param> public void Configure(EntityTypeBuilder<Student> builder) { builder.Property(c => c.Id) .HasColumnName("Id"); builder.Property(c => c.Name) .HasColumnType("varchar(100)") .HasMaxLength(100) .IsRequired(); builder.Property(c => c.Email) .HasColumnType("varchar(100)") .HasMaxLength(11) .IsRequired(); } }
将咱们刚刚建立好的上下文注入到基类仓储中
/// <summary> /// 泛型仓储,实现泛型仓储接口 /// </summary> /// <typeparam name="TEntity"></typeparam> public class Repository<TEntity> : IRepository<TEntity> where TEntity : class { protected readonly StudyContext Db; protected readonly DbSet<TEntity> DbSet; public Repository(StudyContext context) { Db = context; DbSet = Db.Set<TEntity>(); } public virtual void Add(TEntity obj) { DbSet.Add(obj); } public virtual TEntity GetById(Guid id) { return DbSet.Find(id); } public virtual IQueryable<TEntity> GetAll() { return DbSet; } public virtual void Update(TEntity obj) { DbSet.Update(obj); } public virtual void Remove(Guid id) { DbSet.Remove(DbSet.Find(id)); } public int SaveChanges() { return Db.SaveChanges(); } public void Dispose() { Db.Dispose(); GC.SuppressFinalize(this); } }
这个时候咱们知道,由于咱们的应用层的模型的视图模型 StudentViewModel ,可是咱们的仓储接口使用的是 Student 业务领域模型,这个时候该怎么办呢,聪明的你必定会想到我们在上一个系列中所说到的两个知识点,一、DTO的Automapper,而后就是二、引用仓储接口的 IoC 依赖注入,我们今天就先简单配置下 DTO。这两个内容若是不是很清楚,能够翻翻我们以前的系列教程内容。
一、在应用层,新建 AutoMapper 文件夹,咱们之后的配置文件都放到这里,新建DomainToViewModelMappingProfile.cs
/// <summary> /// 配置构造函数,用来建立关系映射 /// </summary> public DomainToViewModelMappingProfile() { CreateMap<Student, StudentViewModel>(); }
这些代码你必定很熟悉的,这里就很少说了,若是一头雾水请看个人第一个系列文章吧。
二、完成 StudentAppService.cs 的设计
namespace Christ3D.Application.Services { /// <summary> /// StudentAppService 服务接口实现类,继承 服务接口 /// 经过 DTO 实现视图模型和领域模型的关系处理 /// 做为调度者,协调领域层和基础层, /// 这里只是作一个面向用户用例的服务接口,不包含业务规则或者知识 /// </summary> public class StudentAppService : IStudentAppService { //注意这里是要IoC依赖注入的,尚未实现 private readonly IStudentRepository _StudentRepository; //用来进行DTO private readonly IMapper _mapper; public StudentAppService( IStudentRepository StudentRepository, IMapper mapper ) { _StudentRepository = StudentRepository; _mapper = mapper; } public IEnumerable<StudentViewModel> GetAll() { return (_StudentRepository.GetAll()).ProjectTo<StudentViewModel>(); } public StudentViewModel GetById(Guid id) { return _mapper.Map<StudentViewModel>(_StudentRepository.GetById(id)); } public void Register(StudentViewModel StudentViewModel) { //判断是否为空等等 尚未实现 _StudentRepository.Add(_mapper.Map<Student>(StudentViewModel)); } public void Update(StudentViewModel StudentViewModel) { _StudentRepository.Update(_mapper.Map<Student>(StudentViewModel)); } public void Remove(Guid id) { _StudentRepository.Remove(id); } public void Dispose() { GC.SuppressFinalize(this); } } }
好啦,其实这个时候,咱们的接口已经可使用了,可能还有些注入呀,没有实现,可是基本的逻辑就这么施行了,你必定看着很熟悉,不管是DTO仍是IOC,不管是EFCore仍是仓储,一切都那么熟悉,可是这就是DDD领域驱动设计么,你必定要带着这个问题好好想一想。答案固然是否认的。
到这里,咱们的核心学习子领域的上下文的建立已经完成,请注意,这是上下文的定义建立完成,里边的核心内容尚未说到。
固然,咱们在完成应用层的调用后,直接就能够用了,这个时候的你可能会发现,到目前为止,我们仍是一个普通的写法,和咱们上个系列是同样的,没有体现出哪里使用了领域驱动设计的思想,无非就是引用了EFCore和定义了一个上下文。
没错,你说的是对的,目前为止尚未实现领域设计的核心,可是至少咱们已经把领域给划分出来了,并且你如何看明白了上边的我说的内容,也应该有必定的想法了,明天我们就重点说说领域事件和聚合的相关概念。
今天重点重申了下DDD的意义,简单说明了下仓储的设计思想,而后也将咱们的项目引入EFCore,并实现了接口等。这里我要说明三点,看看你们读完这篇文章的心情属于哪种:
一、入门:若是你看到我上边的小故事,还对为何使用DDD而疑惑,那就请再仔细看看,好好想一想。不要往下看,就看第一部分。
二、了解:若是你看懂了我说的第一部分的意思,并了解了使用领域驱动设计的意义,可是看下边第三部分的代码结构又好像和平时的多层设计很像,而又去和多层对比,那麻烦请结合个人Git代码看看。
三、优秀:若是你明白了DDD的意义,而且很想了解个人架构究竟是如何进行领域驱动的,恭喜你,已经成功了,剩下的时间我就会带你去深刻了解 中介者模式下的事件驱动——CQRS。
核心内容要来了,你准备好了么 【机智表情】