DDD理論學習系列(12)-- 倉儲

DDD理論學習系列——案例及目錄html


1. 引言

DDD中Repository這個單詞,主要有兩種翻譯:資源庫倉儲,本文取倉儲之譯。web

說到倉儲,咱們確定就想到了倉庫,倉庫通常用來存放貨物,而倉庫通常由倉庫管理員來管理。當工廠生產了一批貨物時,只需交給倉庫管理員便可,他負責貨物的堆放;當須要發貨的時候,倉庫管理員負責從倉庫中撿貨進行貨物出庫處理。當須要庫存盤點時,倉庫管理員負責覈實貨物狀態和庫存。換句話說,倉庫管理員負責了貨物的出入庫管理。經過倉庫管理員這個角色,保證了倉庫和工廠的獨立性,工廠只須要負責生產便可,而至於貨物如何存放工廠無需關注。sql

而咱們要講的倉儲就相似於倉庫管理員,只不過它負責的再也不是貨物的管理,而是聚合的管理,倉儲介於領域模型和數據模型之間,主要用於聚合的持久化和檢索。它隔離了領域模型和數據模型,以便咱們關注於領域模型而不須要考慮如何進行持久化。數據庫

2. DDD中的倉儲

2.1. 倉儲的集合特性

倉儲表明一個聚合的集合,其行爲與.Net集合同樣,倉儲用來存儲和刪除聚合,但同時提供針對聚合的顯式查詢以及彙總。安全

2.2. 倉儲與數據訪問層的區別

  1. 倉儲限定了只能經過聚合根來持久化和檢索領域對象,以確保全部改動和不變性由聚合處理。
  2. 倉儲經過隱藏聚合持久化和檢索的底層技術實現領域層的的持久化無關性(即領域層不須要知道如何持久化領域對象)。
  3. 倉儲在數據模型和領域模型定義了一個邊界。

2.3. 倉儲舉例

下面咱們首先來看一個簡單倉儲的定義:服務器

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);
        }
    }
}

從上面咱們能夠看出,將領域模型的持久化轉移到基礎設施層,隱藏了領域模型的技術複雜性,從而使領域對象可以專一於業務概念和邏輯。架構

2.4. 倉儲的誤解

倉儲也存在不少誤解,許多人認爲其是沒必要要的抽象。當應用於簡單的領域模型時,能夠直接使用持久化框架來進行數據訪問。然而當對複雜的領域模型進行建模時,倉儲是模型的擴展,它代表聚合檢索的意圖,能夠對領域模型進行有意義的讀寫,而不是一個技術框架。app

也有不少人認爲倉儲是一種反模式,由於其隱藏了基礎持久化框架的功能。而恰巧這正是倉儲的要點。基礎持久化框架提供了開放的接口用於對數據模型的查找和修改,而倉儲經過使用定義的命名查詢方法來限制對聚合的訪問。經過使查詢顯式化,就更容易調整查詢,且更重要的是倉儲明確了查詢的意圖,便於領域專家理解。舉個例子:咱們在倉儲中定義了一個方法GetAllActiveUsers()與sql語句select * from users where isactive = 1var users =db.Users.Where(u=>u.IsActive ==1)相比,很明顯倉儲的方法命名就能讓咱們明白了查詢的意圖:查詢全部處於Active狀態的用戶。除了查詢,倉儲僅暴露必要的持久化方法而不是提供全部的CURD方法。框架

2.5. 倉儲的要點

倉儲的要點並非使代碼更容易測試,也不是爲了便於切換底層的持久化存儲方式。固然,在某種程度上,這也的確是倉儲所帶來的利好。倉儲的要點是保持你的領域模型和技術持久化框架的獨立性,這樣你的領域模型能夠隔離來自底層持久化技術的影響。若是沒有倉儲這一層,你的持久化基礎設施可能會泄露到領域模型中,並影響領域模型完整性和最終一致性。

3. 領域模型 VS 數據模型

若是選擇關係型數據庫做爲持久化存儲,咱們能夠藉助於ORM框架來實現領域模型和數據模型之間的映射和持久化操做。

而ORM又是什麼呢?

按照文章開頭中的例子,若是倉儲對應倉庫管理員的角色,那ORM就至關於倉庫機器人,而倉庫就至關於數據庫。爲了方便不一樣商品的歸類存放,對倉庫進行分區,分區就至關於數據表。當公司接到一筆訂單作發貨處理時,銷售員將發貨通知單告知倉庫管理員,倉庫管理員再分配ORM機器人進行撿貨。很顯然,ORM機器人必須可以識別發貨通知單,將發貨通知單中的商品對應到倉庫中存儲的貨物。這裏面發貨通知單就至關於領域模型,而倉庫中存儲的貨物就屬於數據模型。

相信基於上面的比喻,咱們對ORM有了基本的認識。ORM,全稱是Object Relational Mapping,對象關係映射。ORM的前提是,將對象的屬性映射到數據庫字段,將對象之間的引用映射到數據庫表的關係。換句話說,ORM負責將代碼中定義的對象和關係映射到數據庫的表結構中去,並在進行數據訪問時再將表數據映射到代碼中定義的對象,藉助ORM咱們不須要去手動寫SQL語句就能夠完成數據的增刪改查。ORM僅僅抽象了關係數據模型,它只是以面向對象的方式來表示數據模型,以方便咱們在代碼中輕鬆地處理數據。

下面咱們來探討一下數據模型與領域模型的異同。關係數據庫中的數據模型,它由表和列組成,它只是簡單的存儲結構,用於保存領域模型某個時間點的狀態。數據模型能夠分散在幾個表甚至幾個數據庫中。此外,可使用多種形式的持久化存儲,例如文件、web服務器、關係數據庫或NoSQL。領域模型是對問題域的抽象,具備豐富的語言和行爲,由實體和值對象組成。對於一些領域模型,可能與數據模型類似,甚至相同,但在概念上它們是很是不一樣的。ORM與領域模型無關。倉儲的做用就是將領域模型與數據模型分開,而不是讓它們模糊成一個模型。ORM不是倉儲,可是倉儲可使用ORM來持久化領域對象的狀態。

若是你的領域模型與你的數據模型相似,ORM能夠直接映射領域模型到數據存儲,不然,則須要對ORM進行額外的映射配置。

4. 倉儲的定義和實現

上面也提到過,咱們通常在領域層定義倉儲接口,在基礎設施層實現倉儲,以隔離領域模型和數據模型。

4.1. 倉儲方法需明確

倉儲是原則上是領域模型與持久化存儲之間明確的契約,倉儲定義的接口方法不只僅是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);
    }
}

經過以上改造,咱們經過方法的命名來明確查詢的意圖,符合通用語言的規範。

4.2. 泛型倉儲

在實踐中咱們可能會發現,爲每個聚合定義一個倉儲會致使重複代碼,由於大部分的數據操做都是相似的。爲了代碼重用,泛型倉儲就應時而生。

泛型倉儲舉例:

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);
        }
    }
}

經過這種方式,咱們即明確了查詢了意圖,又簡化了代碼。

4.3. IQueryable Vs IEnumerable

在定義倉儲方法的返回值時,咱們可能會比較疑惑,是應該直接返回數據(IEnumerable)仍是返回查詢(IQueryable)以便進行進一步的細化查詢?返回IEnumerable會比較安全,但IQueryable提供了更好的靈活性。事實上,若是使用IQueryable做爲返回值,咱們僅提供一種讀取數據的方法便可進行各類查詢。
可是這種方式就會引入一個問題,就是業務邏輯會滲透到應用層中去,並出現大量重複。好比,在實體中咱們通常使用IsActiveIsDeleted屬性來表示軟刪除,而一旦實體中的某條數據被刪除,那麼UI中基本不會再顯示這條數據,那對於實體的查詢都須要包含相似Where(c=> c.IsActive)的linq表達式。對於這種問題,咱們最好在倉儲中的方法中,好比List()或者ListActive()作默認處理,而不是在應用服務層每次去指定查詢條件。
但具體是返回 IQueryable仍是IEnumerable每一個人的見解不一,具體可參考Repository 返回 IQueryable?仍是 IEnumerable?

5. 事務管理和工做單元

事物管理主要是應用服務層的關注點。然而,由於倉儲和事物管理緊密相關的。倉儲僅關注單一聚合的管理,而一個業務用例可能會涉及到多種的聚合。

事物管理由UOW(Unit of Work)處理。UOW模式的做用是在業務用例的操做中跟蹤聚合的全部更改。一旦發生了更改,UOW就使用事務來協調持久化存儲。爲了確保數據的完整性,若是提交數據失敗,則會回滾全部更改,以確保數據保持有效狀態。

而關於UOW又是一個複雜的話題,咱們後續再講。

6. 倉儲的反模式(注意事項)

  1. 不要支持臨時查詢(ad hoc query)
    倉儲不該該開放擴展,不要爲了支持多種形式的查詢,定義比較寬泛的查詢方法,它不只不能明確表達倉儲查詢的意圖,更可能會致使查詢性能。
  2. 延遲加載是一種設計臭味
    聚合應圍繞不變性構建,幷包含全部必需的屬性去支持不變性。 所以,當加載聚合時,要麼加載全部,要麼一個也不加載。 若是您有一個關係數據庫而且正在使用ORM做爲數據模型,那麼您可能可以延遲加載一些領域對象屬性,這樣就能夠推遲加載不須要的聚合部分。可是,這樣作的問題是,若是您只能部分加載聚合,可能會致使您的聚合邊界錯誤。

  3. 不要使用聚合來實現報表需求
    報表可能會涉及到多個類型的聚合,而倉儲是處理單一聚合的。另外倉儲是基於事務的,可能會致使報表的性能問題。

7. 總結

  1. 倉儲做爲領域模型和數據模型的中介,它負責映射領域模型到持久化存儲。
  2. 倉儲實現了透明持久化,即領域層不須要關注領域對象如何持久化。
  3. 倉儲是一個契約,而不是數據訪問層。它明確代表聚合所必需的數據操做。
  4. ORM框架不是倉儲。倉儲是一種架構模式。ORM用來以面向對象的方式來表示數據模型。倉儲使用ORM來協調領域模型和數據模型。
  5. 倉儲適用於具備豐富領域模型的限界上下文。對於沒有複雜業務邏輯的簡單限界上下文,直接使用持久化框架便可。
  6. 使用UOW進行事務管理。UOW負責跟蹤對象的狀態,倉儲在UOW協調的事務中進行實際的持久化工做。
  7. 倉儲用於管理單個聚合,它不該該控制事務。

參考資料:
領域驅動設計(DDD)的實踐經驗分享之持久化透明
Repository Pattern--A data persistence abstraction
領域驅動設計(DDD)的實踐經驗分享之ORM的思考

相關文章
相關標籤/搜索