C#進階系列——DDD領域驅動設計初探(二):倉儲Repository(上)
前言:上篇介紹了DDD設計Demo裡面的聚合劃分以及實體和聚合根的設計,這章繼續來說說DDD裡面最具爭議的話題之一的倉儲Repository,為什麼Repository會有這麼大的爭議,博主認為主要原因無非以下兩點:一是Repository的真實意圖沒有理解清楚,導致設計的紊亂,隨著專案的橫向和縱向擴充套件,到最後越來越難維護;二是趕時髦的為了“模式”而“模式”,倉儲並非適用於所有專案,這就像沒有任何一種架構能解決所有的設計難題一樣。本篇通過這個設計的Demo來談談博主對倉儲的理解,有不對的地方還望園友們斧正!
DDD領域驅動設計初探系列文章:
一、倉儲的定義
倉儲,顧名思義,儲存資料的倉庫。那麼有人就疑惑了,既然我們有了資料庫來存取資料,為什麼還要弄一個倉儲的概念,其實博主覺得這是一個考慮層面不同的問題,資料庫主要用於存取資料,而倉儲作用之一是用於資料的持久化。從架構層面來說,倉儲用於連線領域層和基礎結構層,領域層通過倉儲訪問儲存機制,而不用過於關心儲存機制的具體細節。按照DDD設計原則,倉儲的作用物件的領域模型的聚合根,也就是說每一個聚合都有一個單獨的倉儲。可能這樣說大家未必能理解,相信看了倉儲的程式碼設計,大家能有一個更加透徹的認識。
二、使用倉儲的意義
1、站在領域層更過關心領域邏輯的層面,上面說了,倉儲作為領域層和基礎結構層的連線元件,使得領域層不必過多的關注儲存細節。在設計時,將倉儲介面放在領域層,而將倉儲的具體實現放在基礎結構層,領域層通過介面訪問資料儲存,而不必過多的關注倉儲儲存資料的細節(也就是說領域層不必關心你用EntityFrameWork還是NHibernate來儲存資料),這樣使得領域層將更多的關注點放在領域邏輯上面。
2、站在架構的層面,倉儲解耦了領域層和ORM之間的聯絡,這一點也就是很多人設計倉儲模式的原因,比如我們要更換ORM框架,我們只需要改變倉儲的實現即可,對於領域層和倉儲的介面基本不需要做任何改變。
三、程式碼示例
1、解決方案結構圖
上面說了,倉儲的設計是介面和實現分離的,於是,我們的倉儲介面和工作單元介面全部放在領域層,在基礎結構層新建了一個倉儲的實現類庫ESTM.Repository,這個類庫需要新增領域層的引用,實現領域層的倉儲介面和工作單元介面。所以,通過上圖可以看到領域層的IRepositories裡面的倉儲介面和基礎結構層ESTM.Repository專案下的Repositories裡面的倉儲實現是一一對應的。下面我們來看看具體的程式碼設計。其實園子裡已有很多經典的倉儲設計,為了更好地說明倉儲的作用,博主還是來班門弄斧下了~~
2、倉儲介面
/// <summary>/// 倉儲介面,定義公共的泛型GRUD /// </summary> /// <typeparam name="TEntity">泛型聚合根,因為在DDD裡面倉儲只能對聚合根做操作</typeparam> public interface IRepository<TEntity> where TEntity : AggregateRoot { #region 屬性 IQueryable<TEntity> Entities { get; } #endregion #region 公共方法 int Insert(TEntity entity); int Insert(IEnumerable<TEntity> entities); int Delete(object id); int Delete(TEntity entity); int Delete(IEnumerable<TEntity> entities); int Update(TEntity entity); TEntity GetByKey(object key); #endregion }
/// <summary> /// 部門聚合根的倉儲介面 /// </summary> public interface IDepartmentRepository:IRepository<TB_DEPARTMENT> { }
/// <summary> /// 選單這個聚合根的倉儲介面 /// </summary> public interface IMenuRepository:IRepository<TB_MENU> { IEnumerable<TB_MENU> GetMenusByRole(TB_ROLE oRole); }
/// <summary> /// 角色這個聚合根的倉儲介面 /// </summary> public interface IRoleRepository:IRepository<TB_ROLE> { }
/// <summary> /// 使用者這個聚合根的倉儲介面 /// </summary> public interface IUserRepository:IRepository<TB_USERS> { IEnumerable<TB_USERS> GetUsersByRole(TB_ROLE oRole); }
除了IRepository這個泛型介面,其他4個倉儲介面都是針對聚合建立的介面, 上章 C#進階系列——DDD領域驅動設計初探(一):聚合 介紹了聚合的劃分,這裡的倉儲介面就是基於此建立。IUserRepository介面實現了IRepository介面,並把對應的聚合根傳入泛型,這裡正好應徵了上章聚合根的設計。
3、倉儲實現類
//倉儲的泛型實現類 public class EFBaseRepository<TEntity> : IRepository<TEntity> where TEntity : AggregateRoot { [Import(typeof(IEFUnitOfWork))] private IEFUnitOfWork UnitOfWork { get; set; } public EFBaseRepository() {
//註冊MEF Regisgter.regisgter().ComposeParts(this); } public IQueryable<TEntity> Entities { get { return UnitOfWork.context.Set<TEntity>(); } } public int Insert(TEntity entity) { UnitOfWork.RegisterNew(entity); return UnitOfWork.Commit(); } public int Insert(IEnumerable<TEntity> entities) { foreach (var obj in entities) { UnitOfWork.RegisterNew(obj); } return UnitOfWork.Commit(); } public int Delete(object id) { var obj = UnitOfWork.context.Set<TEntity>().Find(id); if (obj == null) { return 0; } UnitOfWork.RegisterDeleted(obj); return UnitOfWork.Commit(); } public int Delete(TEntity entity) { UnitOfWork.RegisterDeleted(entity); return UnitOfWork.Commit(); } public int Delete(IEnumerable<TEntity> entities) { foreach (var entity in entities) { UnitOfWork.RegisterDeleted(entity); } return UnitOfWork.Commit(); } public int Update(TEntity entity) { UnitOfWork.RegisterModified(entity); return UnitOfWork.Commit(); } public TEntity GetByKey(object key) { return UnitOfWork.context.Set<TEntity>().Find(key); } }
倉儲的泛型實現類裡面通過MEF匯入工作單元,工作單元裡面擁有連線資料庫的上下文物件。
[Export(typeof(IDepartmentRepository))] public class DepartmentRepository : EFBaseRepository<TB_DEPARTMENT>,IDepartmentRepository { }
[Export(typeof(IMenuRepository))] public class MenuRepository:EFBaseRepository<TB_MENU>,IMenuRepository { public IEnumerable<TB_MENU> GetMenusByRole(TB_ROLE oRole) { throw new Exception(); } }
[Export(typeof(IRoleRepository))] public class RoleRepository:EFBaseRepository<TB_ROLE>,IRoleRepository { }
[Export(typeof(IUserRepository))] public class UserRepository:EFBaseRepository<TB_USERS>,IUserRepository { public IEnumerable<TB_USERS> GetUsersByRole(TB_ROLE oRole) { throw new NotImplementedException(); } }
倉儲是4個具體實現類裡面也可以通過基類裡面匯入的工作單元物件UnitOfWork去操作資料庫。
4、工作單元介面
//工作單元基類介面 public interface IUnitOfWork { bool IsCommitted { get; set; } int Commit(); void Rollback(); }
//倉儲上下文工作單元介面,使用這個的一般情況是多個倉儲之間存在事務性的操作,用於標記聚合根的增刪改狀態 public interface IUnitOfWorkRepositoryContext:IUnitOfWork,IDisposable { /// <summary> /// 將聚合根的狀態標記為新建,但EF上下文此時並未提交 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="obj"></param> void RegisterNew<TEntity>(TEntity obj) where TEntity : AggregateRoot; /// <summary> /// 將聚合根的狀態標記為修改,但EF上下文此時並未提交 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="obj"></param> void RegisterModified<TEntity>(TEntity obj) where TEntity : AggregateRoot; /// <summary> /// 將聚合根的狀態標記為刪除,但EF上下文此時並未提交 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="obj"></param> void RegisterDeleted<TEntity>(TEntity obj) where TEntity : AggregateRoot; }
看到這兩個介面可能有人就有疑惑了,為什麼要設計兩個介面,直接合並一個不行麼?這個工作單元的設計思路來源dax.net的系列文章,再次表示感謝!的確,剛開始,博主也有這種疑惑,仔細思考才知道,應該是出於事件機制來設計的,實現IUnitOfWorkRepositoryContext這個介面的都是針對倉儲設計的工作單元,而實現IUnitOfWork這個介面除了倉儲的設計,可能還有其他情況,比如事件機制。
5、工作單元實現類
//表示EF的工作單元介面,因為DbContext是EF的物件 public interface IEFUnitOfWork : IUnitOfWorkRepositoryContext { DbContext context { get; } }
為什麼要在這裡還設計一層介面?因為博主覺得,工作單元要引入EF的Context物件,同理,如果你用的NH,那麼這裡應該是引入Session物件。
/// <summary> /// 工作單實現類 /// </summary> [Export(typeof(IEFUnitOfWork))] public class EFUnitOfWork : IEFUnitOfWork { #region 屬性 //通過工作單元向外暴露的EF上下文物件 public DbContext context { get { return EFContext; } } [Import(typeof(DbContext))] public DbContext EFContext { get; set; } #endregion #region 建構函式 public EFUnitOfWork() { //註冊MEF Regisgter.regisgter().ComposeParts(this); } #endregion #region IUnitOfWorkRepositoryContext介面 public void RegisterNew<TEntity>(TEntity obj) where TEntity : AggregateRoot { var state = context.Entry(obj).State; if (state == EntityState.Detached) { context.Entry(obj).State = EntityState.Added; } IsCommitted = false; } public void RegisterModified<TEntity>(TEntity obj) where TEntity : AggregateRoot { if (context.Entry(obj).State == EntityState.Detached) { context.Set<TEntity>().Attach(obj); } context.Entry(obj).State = EntityState.Modified; IsCommitted = false; } public void RegisterDeleted<TEntity>(TEntity obj) where TEntity : AggregateRoot { context.Entry(obj).State = EntityState.Deleted; IsCommitted = false; } #endregion #region IUnitOfWork介面 public bool IsCommitted { get; set; } public int Commit() { if (IsCommitted) { return 0; } try { int result = context.SaveChanges(); IsCommitted = true; return result; } catch (DbUpdateException e) { throw e; } } public void Rollback() { IsCommitted = false; } #endregion #region IDisposable介面 public void Dispose() { if (!IsCommitted) { Commit(); } context.Dispose(); } #endregion }
工作單元EFUnitOfWork上面註冊了MEF的Export,是為了供倉儲的實現基類裡面Import,同理,這裡有一點需要注意的,這裡要想匯入DbContext,那麼EF的上下文物件就要Export。
[Export(typeof(DbContext))] public partial class ESTMContainer:DbContext { }
這裡用了萬能的部分類partial,還記得上章說到的領域Model麼,也是在edmx的基礎上通過部分類在定義的。同樣,在edmx的下面肯定有一個EF自動生成的上下文物件,如下:
public partial class ESTMContainer : DbContext { public ESTMContainer() : base("name=ESTMContainer") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { throw new UnintentionalCodeFirstException(); } public DbSet<TB_DEPARTMENT> TB_DEPARTMENT { get; set; } public DbSet<TB_MENU> TB_MENU { get; set; } public DbSet<TB_MENUROLE> TB_MENUROLE { get; set; } public DbSet<TB_ROLE> TB_ROLE { get; set; } public DbSet<TB_USERROLE> TB_USERROLE { get; set; } public DbSet<TB_USERS> TB_USERS { get; set; } }
上文中多個地方用到了註冊MEF的方法
Regisgter.regisgter().ComposeParts(this);
是因為我們在基礎結構層裡面定義了註冊方法
namespace ESTM.Infrastructure.MEF { public class Regisgter { private static object obj =new object(); private static CompositionContainer _container; public static CompositionContainer regisgter() { lock (obj) { try { if (_container != null) { return _container; } AggregateCatalog aggregateCatalog = new AggregateCatalog(); string path = AppDomain.CurrentDomain.BaseDirectory; var thisAssembly = new DirectoryCatalog(path, "*.dll"); if (thisAssembly.Count() == 0) { path = path + "bin\\"; thisAssembly = new DirectoryCatalog(path, "*.dll"); } aggregateCatalog.Catalogs.Add(thisAssembly); _container = new CompositionContainer(aggregateCatalog); return _container; } catch (Exception ex) { return null; } } } } }
6、Demo測試
為了測試我們搭的框架能執行通過,我們在應用層裡面寫一個測試方法。正常情況下,應用層ESTM.WCF.Service專案只需要新增ESTM.Domain專案的引用,那麼在應用層裡面如何找到倉儲的實現呢?還是我們萬能的MEF,通過IOC依賴注入的方式,應用層不必新增倉儲實現層的引用,通過MEF將倉儲實現注入到應用層裡面,但前提是應用層的bin目錄下面要有倉儲實現層生成的dll,需要設定ESTM.Repository專案的生成目錄為ESTM.WCF.Service專案的bin目錄。這個問題在C#進階系列——MEF實現設計上的“鬆耦合”(終結篇:面向介面程式設計)這篇裡面介紹過。
還是來看看測試程式碼
namespace ESTM.WCF.Service { class Program { [Import] public IUserRepository userRepository { get; set; } static void Main(string[] args) { var oProgram = new Program(); Regisgter.regisgter().ComposeParts(oProgram); var lstUsers = oProgram.userRepository.Entities.ToList(); } } }
執行得到結果:
7、總結
至此,我們框架倉儲的大致設計就完了,我們回過頭來看看這樣設計的優勢所在:
(1)倉儲介面層和實現層分離,使得領域模型更加純淨,領域模型只關注倉儲的介面,而不用關注資料儲存的具體細節,使得領域模型將更多的精力放在領域業務上面。
(2)應用層只需要引用領域層,只需要呼叫領域層裡面的倉儲介面就能得到想要的資料,而不用新增倉儲具體實現的引用,這也正好符合專案解耦的設計。
(3)更換ORM方便。專案現在用的是EF,若日後需要更換成NH,只需要再實現一套倉儲和上下文即可。這裡需要說明一點,由於整個框架使用EF的model First,為了直接使用EF的model,我們把edmx定義在了領域層裡面,其實這樣是不合理的,但是我們為了使用簡單,直接用了partial定義領域模型的行為,如果要更好的使用DDD的設計,EF現在的Code First是最好的方式,領域層裡面只定義領域模型和關注領域邏輯,EF的CRUD放在基礎結構層,切換ORM就真的只需要重新實現一套倉儲即可,這樣的設計才是博主真正想要的效果,奈何時間和經歷有限,敬請諒解。以後如果有時間博主會分享一個完整設計的DDD。
相關推薦
C#進階系列——DDD領域驅動設計初探(六):領域服務
前言:之前一直在搭建專案架構的程式碼,有點偏離我們的主題(DDD)了,這篇我們繼續來聊聊DDD裡面另一個比較重要的知識點:領域服務。關於領域服務的使用,書中也介紹得比較晦澀,在此就根據博主自己的理解談談這個知識點的使用。 DDD領域驅動設計初探系列文章: 一、領域服務的引入 在《領域驅動設計:軟體核
C#進階系列——DDD領域驅動設計初探(四):WCF搭建
前言:前面三篇分享了下DDD裡面的兩個主要特性:聚合和倉儲。領域層的搭建基本完成,當然還涉及到領域事件和領域服務的部分,後面再專案搭建的過程中慢慢引入,博主的思路是先將整個架構走通,然後一步一步來新增相關元素,使架構慢慢變得豐滿。這篇打算分享下應用層的搭建。根據DDD的設計原則,應用層不包含任何領域邏輯,它主
C#進階系列——DDD領域驅動設計初探(五):AutoMapper使用
前言:前篇搭建了下WCF的程式碼,就提到了DTO的概念,對於為什麼要有這麼一個DTO的物件,上章可能對於這點不太詳盡,在此不厭其煩再來提提它的作用: 從安全上面考慮,領域Model都帶有領域業務,讓Client端引用Domain Model就意味著Client端可以繞過應用層直接完成業務邏輯的呼叫,這樣
C#進階系列——DDD領域驅動設計初探(三):倉儲Repository(下)
前言:上篇介紹了下倉儲的程式碼架構示例以及簡單分析了倉儲了使用優勢。本章還是繼續來完善下倉儲的設計。上章說了,倉儲的最主要作用的分離領域層和具體的技術架構,使得領域層更加專注領域邏輯。那麼涉及到具體的實現的時候我們應該怎麼做呢,本章就來說說倉儲裡面具體細節方便的知識。 DDD領域驅動設計初探系列文章:
C#進階系列——DDD領域驅動設計初探(二):倉儲Repository(上)
前言:上篇介紹了DDD設計Demo裡面的聚合劃分以及實體和聚合根的設計,這章繼續來說說DDD裡面最具爭議的話題之一的倉儲Repository,為什麼Repository會有這麼大的爭議,博主認為主要原因無非以下兩點:一是Repository的真實意圖沒有理解清楚,導致設計的紊亂,隨著專案的橫向和縱向擴充套件,
C#進階系列——DDD領域驅動設計初探(七):Web層的搭建
前言:好久沒更新部落格了,每天被該死的業務纏身,今天正好一個模組完成了,繼續來完善我們的程式碼。之前的六篇完成了領域層、應用層、以及基礎結構層的部分程式碼,這篇打算搭建下UI層的程式碼。 DDD領域驅動設計初探系列文章: 一、UI層介紹 在DDD裡面,UI層的設計也分為BS和CS,本篇還是以Web為
C#進階系列——DDD領域驅動設計初探(一):聚合
前言:又有差不多半個月沒寫點什麼了,感覺這樣很對不起自己似的。今天看到一篇博文裡面寫道:越是忙人越有時間寫部落格。呵呵,似乎有點道理,博主為了證明自己也是忙人,這不就來學習下DDD這麼一個聽上去高大上的東西。前面介紹了下MEF和AOP的相關知識,後面打算分享Automapper、倉儲模式、WCF等東西的,可是
EF Code first 和 DDD (領域驅動設計研究)系列一
發的 tex bsp cti 設計 ron 映射 developer devel 在上個公司工作時,開發公司產品的過程中,接觸到了EF Code first. 當時,整個產品的架構都是Lead developer設計建立的,自己也不是特別理解,就趕鴨子上架跟著一起開發了。
【9】C++進階系列(泛型設計以及STL標準模板庫)
1、泛型程式設計基本概念 泛型程式設計:編寫不依賴與具體資料型別的程式,將演算法從特定的資料結構中抽象出來,成為通用的。C++的模板為泛型程式設計定義了關鍵的基礎。 兩個術語:概念,模型 概念:用來界定具備一定功能的資料型別,例如:將“可以比較大小的所有資料型別(有比較
C#進階系列——MEF實現設計上的“鬆耦合”(二)
前言:前篇 C#進階系列——MEF實現設計上的“鬆耦合”(一) 介紹了下MEF的基礎用法,讓我們對MEF有了一個抽象的認識。當然MEF的用法可能不限於此,比如MEF的目錄服務、目錄篩選、重組部件等高階應用在這裡就不做過多講解,因為博主覺得這些用法只有在某些特定的環境下面才會用到,著實不太普遍,感覺沒有鑽下去的
C#進階系列——MEF實現設計上的“鬆耦合”(一)
前言:最近去了趟外地出差,介紹推廣小組開發的框架類產品。推廣物件是本部門在專案上面的同事——1到2年工作經驗的初級程式設計師。在給他們介紹框架時發現很多框架設計層面的知識他們都沒有接觸過,甚至沒聽說過,這下囧了~~於是乎在想該如何跟他們解釋MEF、AOP、倉儲模式等方面的東東。本來 C#基礎系列 應該還有兩篇
C#進階系列——MEF實現設計上的“鬆耦合”(四):建構函式注入
前言:今天十一長假的第一天,本因出去走走,奈何博主最大的樂趣是假期坐在電腦前看各處堵車,順便寫寫部落格,有點收穫也是好的。關於MEF的知識,之前已經分享過三篇,為什麼有今天這篇?是因為昨天分享領域服務的時候,用到MEF的注入有參建構函式的方法,博主好奇心重,打算稍微深挖一下,這篇來對此知識點做個總結。 還是
C#進階系列——MEF實現設計上的“鬆耦合”(終結篇:面向介面程式設計)
序:忙碌多事的八月帶著些許的倦意早已步入尾聲,金秋九月承載著抗戰勝利70週年的喜慶撲面而來。沒來得及任何準備,似乎也不需要任何準備,因為生活不需要太多將來時。每天忙著上班、加班、白加班,忘了去憤,忘了去算計所謂的價值。天津爆炸事故時刻警示著我們生命的無常,逝者安息,活著的人生活還得繼續,珍惜生命,遠離傷害。武
C#進階系列——WebApi 異常處理解決方案(轉)
機制 輸出 ges 如果 但是 rom lba slist 解決 出處:http://www.cnblogs.com/landeanfen/p/5363846.html 閱讀目錄 一、使用異常篩選器捕獲所有異常 二、HttpResponseException自
C#進階系列——WebApi 接口測試工具:WebApiTestClient
spa type 區域 all 手動 shee 找到 網絡 打開文件 C#進階系列——WebApi 接口測試工具:WebApiTestClient 前言:這兩天在整WebApi的服務,由於調用方是Android客戶端,Android開發人員也不懂C#語法,API裏
C#進階系列——WebApi 路由機制剖析:你準備好了嗎?
事先 blank path can tex 全局配置 dex 找不到 save 前言:從MVC到WebApi,路由機制一直是伴隨著這些技術的一個重要組成部分。 它可以很簡單:如果你僅僅只需要會用一些簡單的路由,如/Home/Index,那麽你只需要配置一個默認路由就能簡
C#進階系列——WebApi 跨域問題解決方案:CORS
dea ati ice pro target default 異常 測試工具 復雜 前言:上篇總結了下WebApi的接口測試工具的使用,這篇接著來看看WebAPI的另一個常見問題:跨域問題。本篇主要從實例的角度分享下CORS解決跨域問題一些細節。 WebApi系列文章
[轉]C#進階系列——WebApi 接口返回值不困惑:返回值類型詳解
try 接口測試工具 des rep home creat port 調用 學習 本文轉自:http://www.cnblogs.com/landeanfen/p/5501487.html 閱讀目錄 一、void無返回值 二、IHttpActionResult
C#進階系列——WebApi 身份認證解決方案:Basic基礎認證
str 常見 bre 這一 dex ace timeout ticket 結合 閱讀目錄 一、為什麽需要身份認證 二、Basic基礎認證的原理解析 1、常見的認證方式 2、Basic基礎認證原理 三、Basic基礎認證的代碼示例 1、登錄過程 2、/Home/I
C#進階系列——WebApi 接口參數不再困惑:傳參詳解
pub 博客 bapi write ids 簡單 指定 數組 這也 https://www.cnblogs.com/landeanfen/p/5337072.html 閱讀目錄 一、get請求 1、基礎類型參數 2、實體作為參數 3、數組作為參數 4