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

来源:http://www.cnblogs.com/sheng-jie/archive/2017/07/31/7261286.html
-Advertisement-
Play Games

"DDD理論學習系列——案例及目錄" 1. 引言 DDD中 Repository 這個單詞,主要有兩種翻譯: 資源庫 和 倉儲 ,本文取 倉儲 之譯。 說到倉儲,我們肯定就想到了倉庫,倉庫一般用來存放貨物,而倉庫一般由倉庫管理員來管理。當工廠生產了一批貨物時,只需交給倉庫管理員即可,他負責貨物的堆放 ...


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


1. 引言

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

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

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

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介面,它扮演一個的網關角色,負責領域模型和數據模型的映射。

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. 倉儲的誤解

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

也有很多人認為倉儲是一種反模式,因為其隱藏了基礎持久化框架的功能。而恰巧這正是倉儲的要點。基礎持久化框架提供了開放的介面用於對數據模型的查找和修改,而倉儲通過使用定義的命名查詢方法來限制對聚合的訪問。通過使查詢顯式化,就更容易調整查詢,且更重要的是倉儲明確了查詢的意圖,便於領域專家理解。舉個例子:我們在倉儲中定義了一個方法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的思考


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 首先,記憶體模型圖,如下: 其次,一句話概括各個區域的作用: 1:程式計數器(Program Counter Register),讓虛擬機中的位元組碼解釋器通過改變計數器的值來獲取下一條代碼指令,比如分支、迴圈、跳轉、異常處理、線程恢復等; 2:Java 虛擬機棧(Java Virtual Machin... ...
  • 在前面的Java JDBC的基礎知識(二)和(三)中,主要介紹JDBC的原理和簡單的應用過程。尤其在(二)中,可以發現代碼進行多次try/catch,還有在前面創建連接等過程中好多參數我都給寫定了。 這些參數本來可以是在調用的時候再給的。以前學習過將工具類和測試類分開寫的好處,下麵就介紹資料庫的工具 ...
  • 引言: 在項目上傳文件根據項目需求使用了 WebUploader , 遇到了跨域,發現總是上傳失敗, 在網上找了許多的博客, 很少有正確的, 並且解釋的對我這種渣來說比較捉急, 最終通過整理及實踐解決了問題, 遂把解決方案貼出來,希望能幫助到其他遇到此問題的朋友. 1: 在使用WebUploader ...
  • 時間限制: 1 s 空間限制: 128000 KB 題目等級 : 鑽石 Diamond 題解 查看運行結果 時間限制: 1 s 空間限制: 128000 KB 題目等級 : 鑽石 Diamond 時間限制: 1 s 空間限制: 128000 KB 題目等級 : 鑽石 Diamond 時間限制: 1 ...
  • "DDD理論學習系列——案例及目錄" 1. 引言 Module,即模塊,是指提供特定功能的相對獨立的單元。提到模塊,你肯定就會想到模塊化設計思想,也就是功能的分解和組合。對於簡單問題,可以直接構建單一模塊的程式。而對於複雜問題,則可以先創建若幹個較小的模塊,然後將它們組裝、鏈接在一起,從而構成複雜的 ...
  • 網路管理員不再擁有配置物理路由器,交換機和其他LAN / WAN組件的舒適區域。我們現在生活在一個虛擬化世界中,管理員必須挖掘VMware,Microsoft,Red Hat等虛擬化平臺中的網路組件。 今天,企業IT 對容器越來越感興趣,這些容器需要強大的網路技能才能正確配置容器架構。在本文中,我... ...
  • (๑´ڡ`๑) 最常用的方式是組合使用構造函數與原型模式,原型模式的思想非常重要,詳細理解 ...
  • 目錄 · 大型網站軟體系統的特點 · 大型網站架構演化發展歷程 · 初始階段的網站架構 · 需求/解決問題 · 架構 · 應用服務和數據服務分離 · 需求/解決問題 · 架構 · 使用緩存改善網站性能 · 需求/解決問題 · 架構 · 使用應用伺服器集群改善網站的併發處理能力 · 需求/解決問題 · ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...