哈嘍你們好喲,今天又到了老張的週二四放送時間了,固然中間還有不按期的更新(由於我的看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。
核心內容要來了,你準備好了麼 【機智表情】