DDD理論學習系列——案例及目錄html
DDD中Repository這個單詞,主要有兩種翻譯:資源庫和倉儲,本文取倉儲之譯。web
說到倉儲,咱們確定就想到了倉庫,倉庫通常用來存放貨物,而倉庫通常由倉庫管理員來管理。當工廠生產了一批貨物時,只需交給倉庫管理員便可,他負責貨物的堆放;當須要發貨的時候,倉庫管理員負責從倉庫中撿貨進行貨物出庫處理。當須要庫存盤點時,倉庫管理員負責覈實貨物狀態和庫存。換句話說,倉庫管理員負責了貨物的出入庫管理。經過倉庫管理員這個角色,保證了倉庫和工廠的獨立性,工廠只須要負責生產便可,而至於貨物如何存放工廠無需關注。sql
而咱們要講的倉儲就相似於倉庫管理員,只不過它負責的再也不是貨物的管理,而是聚合的管理,倉儲介於領域模型和數據模型之間,主要用於聚合的持久化和檢索。它隔離了領域模型和數據模型,以便咱們關注於領域模型而不須要考慮如何進行持久化。數據庫
倉儲表明一個聚合的集合,其行爲與.Net集合同樣,倉儲用來存儲和刪除聚合,但同時提供針對聚合的顯式查詢以及彙總。安全
下面咱們首先來看一個簡單倉儲的定義:服務器
namespace DomainModel { public interface ICustomerRepository { Customer FindBy(Guid id); void Add(Customer customer); void Remove(Customer customer); } }
一般來講,倉儲由應用服務層調用。倉儲定義應用服務執行業務用例時須要的全部的數據訪問方法。而倉儲的實現一般位於基礎架構層,由持久化框架來支撐。如下的倉儲實現是藉助於ORM框架Nhibernate的ISession
接口,它扮演一個的網關角色,負責領域模型和數據模型的映射。session
namespace Infrastructure.Persistence { public class CustomerRepository : ICustomerRepository { private ISession _session; public CustomerRepository (ISession session) { _session = session; } public IEnumerable<Customer> FindBy (Guid id) return _session.Load<Order> (id); } public void Add (Customer customer) { _session.Save (customer); } public void Remove (Customer customer) { _session.Delete (customer); } } }
從上面咱們能夠看出,將領域模型的持久化轉移到基礎設施層,隱藏了領域模型的技術複雜性,從而使領域對象可以專一於業務概念和邏輯。架構
倉儲也存在不少誤解,許多人認爲其是沒必要要的抽象。當應用於簡單的領域模型時,能夠直接使用持久化框架來進行數據訪問。然而當對複雜的領域模型進行建模時,倉儲是模型的擴展,它代表聚合檢索的意圖,能夠對領域模型進行有意義的讀寫,而不是一個技術框架。app
也有不少人認爲倉儲是一種反模式,由於其隱藏了基礎持久化框架的功能。而恰巧這正是倉儲的要點。基礎持久化框架提供了開放的接口用於對數據模型的查找和修改,而倉儲經過使用定義的命名查詢方法來限制對聚合的訪問。經過使查詢顯式化,就更容易調整查詢,且更重要的是倉儲明確了查詢的意圖,便於領域專家理解。舉個例子:咱們在倉儲中定義了一個方法GetAllActiveUsers()
與sql語句select * from users where isactive = 1
或var users =db.Users.Where(u=>u.IsActive ==1)
相比,很明顯倉儲的方法命名就能讓咱們明白了查詢的意圖:查詢全部處於Active狀態的用戶。除了查詢,倉儲僅暴露必要的持久化方法而不是提供全部的CURD方法。框架
倉儲的要點並非使代碼更容易測試,也不是爲了便於切換底層的持久化存儲方式。固然,在某種程度上,這也的確是倉儲所帶來的利好。倉儲的要點是保持你的領域模型和技術持久化框架的獨立性,這樣你的領域模型能夠隔離來自底層持久化技術的影響。若是沒有倉儲這一層,你的持久化基礎設施可能會泄露到領域模型中,並影響領域模型完整性和最終一致性。
若是選擇關係型數據庫做爲持久化存儲,咱們能夠藉助於ORM框架來實現領域模型和數據模型之間的映射和持久化操做。
而ORM又是什麼呢?
按照文章開頭中的例子,若是倉儲對應倉庫管理員的角色,那ORM就至關於倉庫機器人,而倉庫就至關於數據庫。爲了方便不一樣商品的歸類存放,對倉庫進行分區,分區就至關於數據表。當公司接到一筆訂單作發貨處理時,銷售員將發貨通知單告知倉庫管理員,倉庫管理員再分配ORM機器人進行撿貨。很顯然,ORM機器人必須可以識別發貨通知單,將發貨通知單中的商品對應到倉庫中存儲的貨物。這裏面發貨通知單就至關於領域模型,而倉庫中存儲的貨物就屬於數據模型。
相信基於上面的比喻,咱們對ORM有了基本的認識。ORM,全稱是Object Relational Mapping,對象關係映射。ORM的前提是,將對象的屬性映射到數據庫字段,將對象之間的引用映射到數據庫表的關係。換句話說,ORM負責將代碼中定義的對象和關係映射到數據庫的表結構中去,並在進行數據訪問時再將表數據映射到代碼中定義的對象,藉助ORM咱們不須要去手動寫SQL語句就能夠完成數據的增刪改查。ORM僅僅抽象了關係數據模型,它只是以面向對象的方式來表示數據模型,以方便咱們在代碼中輕鬆地處理數據。
下面咱們來探討一下數據模型與領域模型的異同。關係數據庫中的數據模型,它由表和列組成,它只是簡單的存儲結構,用於保存領域模型某個時間點的狀態。數據模型能夠分散在幾個表甚至幾個數據庫中。此外,可使用多種形式的持久化存儲,例如文件、web服務器、關係數據庫或NoSQL。領域模型是對問題域的抽象,具備豐富的語言和行爲,由實體和值對象組成。對於一些領域模型,可能與數據模型類似,甚至相同,但在概念上它們是很是不一樣的。ORM與領域模型無關。倉儲的做用就是將領域模型與數據模型分開,而不是讓它們模糊成一個模型。ORM不是倉儲,可是倉儲可使用ORM來持久化領域對象的狀態。
若是你的領域模型與你的數據模型相似,ORM能夠直接映射領域模型到數據存儲,不然,則須要對ORM進行額外的映射配置。
上面也提到過,咱們通常在領域層定義倉儲接口,在基礎設施層實現倉儲,以隔離領域模型和數據模型。
倉儲是原則上是領域模型與持久化存儲之間明確的契約,倉儲定義的接口方法不只僅是CURD方法。它是領域模型的擴展,並以領域專家所理解的術語編寫。倉儲接口的定義應該根據應用程序的用例需求來建立,而不是從相似CURD的數據訪問角度來構建。
咱們來看一段代碼:
namespace DomainModel { public interface ICustomerRepository { Customer FindBy (Guid id); IEnumerable<Customer> FindAllThatMatch (Query query); IEnumerable<Customer> FindAllThatMatch (String hql); void Add (Customer customer); } }
以上倉儲定義了一個FindAllThatMatch
方法以支持客戶端以任何方式查詢領域對象。這個方法的設計思想無可置否,靈活且能夠擴展,可是它並無明確的代表查詢的意圖,咱們就失去了對查詢的控制。爲了真正瞭解如何使用這些方法,開發人員須要跟蹤相關調用堆棧,才能知悉方法的意圖,更別說出現性能問題時如何着手優化了。由於倉儲定義的接口方法過於寬泛且不具體,它模糊了領域的的概念,因此定義這樣的一個接口方法是無心義的。
咱們能夠以下改造:
namespace DomainModel { public interface ICustomerRepository { Customer FindBy (Guid id); IEnumerable<Customer> FindAllThatAreDeactivated (); IEnumerable<Customer> FindAllThatAreOverAllowedCredit (); void Add (Customer customer); } }
經過以上改造,咱們經過方法的命名來明確查詢的意圖,符合通用語言的規範。
在實踐中咱們可能會發現,爲每個聚合定義一個倉儲會致使重複代碼,由於大部分的數據操做都是相似的。爲了代碼重用,泛型倉儲就應時而生。
泛型倉儲舉例:
namespace DomainModel { public interface IRepository<T> where T : EntityBase { T GetById (int id); IEnumerable<T> List (); IEnumerable<T> List (Expression<Func<T, bool>> predicate); void Add (T entity); void Delete (T entity); void Edit (T entity); } public abstract class EntityBase { public int Id { get; protected set; } } }
泛型倉儲實現:
namespace Infrastructure.Persistence { public class Repository<T> : IRepository<T> where T : EntityBase { private readonly ApplicationDbContext _dbContext; public Repository (ApplicationDbContext dbContext) { _dbContext = dbContext; } public virtual T GetById (int id) { return _dbContext.Set<T> ().Find (id); } public virtual IEnumerable<T> List () { return _dbContext.Set<T> ().AsEnumerable (); } public virtual IEnumerable<T> List (Expression<Func<T, bool>> predicate) { return _dbContext.Set<T> () .Where (predicate) .AsEnumerable (); } public void Insert (T entity) { _dbContext.Set<T> ().Add (entity); _dbContext.SaveChanges (); } public void Update (T entity) { _dbContext.Entry (entity).State = EntityState.Modified; _dbContext.SaveChanges (); } public void Delete (T entity) { _dbContext.Set<T> ().Remove (entity); _dbContext.SaveChanges (); } } }
經過定義泛型倉儲和默認的實現,很大程度上進行了代碼重用。可是,嘗試將泛型倉儲應用全部倉儲並非一個好的主意。對於簡單的聚合咱們能夠直接使用泛型倉儲來簡化代碼。但對於複雜的聚合,泛型倉儲可能就會不太適合,若是基於泛型倉儲的方法進行數據訪問,就會模糊對聚合的訪問意圖。
對於複雜的聚合,咱們能夠從新定義:
namespace DomainModel { public interface ICustomerRepository { Customer FindBy (Guid id); IEnumerable<Customer> FindAllThatAreDeactivated (); void Add (Customer customer); } }
在實現時,咱們能夠引用泛型倉儲來避免代碼重複。
namespace Infrastructure.Persistence { public class CustomerRepository : ICustomerRepository { private IRepository<Customer> _customersRepository; public Customers (IRepository<Customer> customersRepository) { _customersRepository = customersRepository; } // .... public IEnumerable<Customer> FindAllThatAreDeactivated () { _customersRepository.List(c => c.IsActive == false); } public void Add (Customer customer) { _customersRepository.Add (customer); } } }
經過這種方式,咱們即明確了查詢了意圖,又簡化了代碼。
在定義倉儲方法的返回值時,咱們可能會比較疑惑,是應該直接返回數據(IEnumerable)仍是返回查詢(IQueryable)以便進行進一步的細化查詢?返回IEnumerable
會比較安全,但IQueryable
提供了更好的靈活性。事實上,若是使用IQueryable
做爲返回值,咱們僅提供一種讀取數據的方法便可進行各類查詢。
可是這種方式就會引入一個問題,就是業務邏輯會滲透到應用層中去,並出現大量重複。好比,在實體中咱們通常使用IsActive
或IsDeleted
屬性來表示軟刪除,而一旦實體中的某條數據被刪除,那麼UI中基本不會再顯示這條數據,那對於實體的查詢都須要包含相似Where(c=> c.IsActive)
的linq表達式。對於這種問題,咱們最好在倉儲中的方法中,好比List()
或者ListActive()
作默認處理,而不是在應用服務層每次去指定查詢條件。
但具體是返回 IQueryable仍是IEnumerable每一個人的見解不一,具體可參考Repository 返回 IQueryable?仍是 IEnumerable?。
事物管理主要是應用服務層的關注點。然而,由於倉儲和事物管理緊密相關的。倉儲僅關注單一聚合的管理,而一個業務用例可能會涉及到多種的聚合。
事物管理由UOW(Unit of Work)處理。UOW模式的做用是在業務用例的操做中跟蹤聚合的全部更改。一旦發生了更改,UOW就使用事務來協調持久化存儲。爲了確保數據的完整性,若是提交數據失敗,則會回滾全部更改,以確保數據保持有效狀態。
而關於UOW又是一個複雜的話題,咱們後續再講。
延遲加載是一種設計臭味
聚合應圍繞不變性構建,幷包含全部必需的屬性去支持不變性。 所以,當加載聚合時,要麼加載全部,要麼一個也不加載。 若是您有一個關係數據庫而且正在使用ORM做爲數據模型,那麼您可能可以延遲加載一些領域對象屬性,這樣就能夠推遲加載不須要的聚合部分。可是,這樣作的問題是,若是您只能部分加載聚合,可能會致使您的聚合邊界錯誤。
不要使用聚合來實現報表需求
報表可能會涉及到多個類型的聚合,而倉儲是處理單一聚合的。另外倉儲是基於事務的,可能會致使報表的性能問題。
參考資料:
領域驅動設計(DDD)的實踐經驗分享之持久化透明
Repository Pattern--A data persistence abstraction
領域驅動設計(DDD)的實踐經驗分享之ORM的思考