倉儲的實現:基本篇
我們先從技術角度考慮倉儲的問題。實體框架(EntityFramework)中,操作數據庫是非常簡單的:在ObjectContext中使用LINQ to Entities即可完成操作。開發人員也不需要爲事務管理而操心,一切都由EF包辦。與原本的ADO.NET以及LINQ to SQL相比,EF更爲簡單,LINQ to Entities的引入使得軟件開發變得更爲“領域化”。
下面的代碼測試了持久化一個 Customer實體,並從持久化機制中查詢這個Customer實體的正確性。從代碼中可以看到,我們用了一種很自然的表達方式,表述了“我希望查詢一個名字爲Sunny的客戶”這樣一種業務邏輯。
如果你需要實現的系統並不複雜,那麼按上面的方式添加、查詢實體也不會有太大問題,你可以在 ObjectContext中隨心所欲地使用LINQ to Entities來方便地得到你需要的東西,更讓人興奮的是,.NET 4.0允許支持並行計算的PLINQ,如果你的計算機具有多核處理器,你將非常方便地獲得效率上的提升。然而,當你的架構需要考慮下面幾個方面時,單純的 LINQ to Entities方式就無法滿足需求了:
- 領域模型與技術架構分離。這是DDD的一貫宗旨,也就是說,領域模型中是不能混入任何技術架構實現的,業務和技術必須嚴格分離。考察以上實現,領域模型緊密依賴於實體框架,而目前實體框架並非是完全領域驅動的,它更偏向於一種技術架構。比如上面的Customer實體,在實體框架驅動的設計中,它已經被EF“牽着鼻子走”了
- 規約(Specification)模式的引入。以上實現中,雖然LINQ使得業務邏輯的表述方式更爲“領域化”,可以看成是一種 Domain Specific Language(Microsoft Dynamics AX早已引入了類似的語言集成的語法),但這種做法會使得模型對領域概念的描述變得難以更改。比如:可以用“from employee in employees where employee.Age >= 60 && employee.Gender.Equals(Gender.Male) select employee”來表述“找出所有男性退休職工”的概念,但這種邏輯是寫死在領域模型中的,倘若某天男性退休的年齡從60歲調至55歲,那麼上面的查詢就不正確了,此時不得不對領域模型作修改。更可怕的是,LINQ to Entities仍然沒有避免“SQL everywhere”的難處,領域模型中將到處充斥這這種LINQ查詢,弊端也不多說了。解決方法就是引入規約模式
- 倉儲實現的可擴展性。比如如果經過系統分析,發現今後可能需要用到其它的持久化解決方案,那麼你就不能直接使用實體框架
於是,也就回到了上篇博客中我描述的問題:倉儲不是Data Object,也不僅僅是進行數據庫CRUD操作的Data Manager,它承擔瞭解耦領域模型和技術架構的重要職責。爲了完美地解決上面提到的問題,我們仍然採用領域驅動設計中倉儲的設計模式,而將實體框架作爲倉儲的具體實現部分。在詳細介紹倉儲的設計與實現之前,讓我們回顧一下上文最後部分我提到的那個倉儲的接口:
在本文的案例中,倉儲是這樣實現的:
-
將上述倉儲接口定義在實體、值對象和服務所在的領域層。有朋友問過我,既然倉儲需要與外部存儲機制打交道,那麼它必定需要知道技術架構方面的細節,而將其定義在領域層,就會使得領域層依賴於具體的技術實現方式,這樣就會使領域層變得“不純淨” 了。其實不然!請注意,我們這裏僅僅只是將倉儲的接口定義在了領域層,而不是倉儲的具體實現(Concrete Repository)。更通俗地說,接口作爲系統架構的基礎元素,決定了整個系統的架構模式,而基於接口的具體實現只不過是一種可替換的組件,它不能成爲系統架構中的一部分。由於領域層需要用到倉儲,我便將倉儲的接口定義在了領域層。當然,從.NET的實現技術考慮,你可以新建一個Class Library,並將上述接口定義在這個Class Library中,然後在領域層和倉儲的具體實現中分別引用這個Class Library
-
新建一個Class Library(在本文的案例中,命名爲EasyCommerce.Infrastructure.Repositories),添加對領域層 assembly的引用,並實現上述接口。由於我們採用實體框架作爲倉儲的具體實現,因此,將這個倉儲命名爲EdmRepository(Entity Data Model Repository)。EdmRepository有着類似如下的實現:
從上面的代碼可以看到,EdmRepository將實體框架抽象到ObjectContext這一層,這也使我們沒法通過LINQ to Entities來查詢模型中的對象。幸運的是,ObjectContext爲我們提供了一系列函數,用以實現實體的CRUD。爲了使用這些函數,我們需要知道與實體相關的EntitySetName,爲此,我定義了一個AggregateRootAttribute,並將其應用在聚合根上,以便在對實體進行操作的時候,能夠正確地獲得EntitySetName。類似的代碼如下:
回頭來看EdmRepository的構造函數,在構造函數中,我們使用.NET的反射機制獲得了定義在聚合根類型上的EntitySetName
-
使用IoC/DI(控制反轉/依賴注入)框架,將倉儲的實現(EdmRepository)注射到領域模型中。至此,領域模型一直保持着對倉儲接口的引用,而對倉儲的具體實現方式一無所知。由於IoC/DI的引入,我們得到了一個純淨的領域模型。在這裏我也想提出一個衡量系統架構優劣度的重要指標,就是領域模型的純淨度。常見的 IoC/DI框架有Spring.NET和Castle Windsor MicroKernel。在本文的案例中,我採用了Castle Windsor。以下是針對Castle Windsor的配置文件片段:
通過這個配置片段我們還可以看到,在框架創建針對“客戶”實體的倉儲實例時,我們案例中的領域模型容器(EntitiesContainer)也以構造器注入的方式,被注射到了EdmRepository的構造函數中。接下來我們做一個單體測試:
考察上面的代碼,倉儲的使用者(Client,可以是領域模型中的任何對象)對倉儲的具體實現一無所知
總結
總之,倉儲的實現可以用下圖表述:
回頭來看本文剛開始的三個問題:依賴注入可以解決問題1和3,而倉儲接口的引入,也使得規約模式的應用成爲可能。.NET中有一個泛型委託,稱爲Func<T, bool>,它可以作爲LINQ的where子句參數,實現類似規約的功能。有關規約模式,我將在其它的文章中討論。
從本文還可以瞭解到,依賴注入是維持領域模型純淨度的一大利器;另一大利器是領域事件,我將在後續的文章中詳述。對於本文開始的第三個問題,也就是倉儲實現的可擴展性,將在下篇文章中進行討論,包括的內容有:事務處理和可擴展的倉儲框架的實現。
-----【以下爲原文網友評論及回覆信息】----- |
[ 2010-2-24 11:22:00 | By: 文野(遊客) ] 你好,我同意衡量系統架構優劣度的重要指標,就是領域模型的純淨度這個觀點。但我一直有一些疑惑,很多ORM框架,或象上面例子中,給領域對象添加一些Attribute,作爲實體在持久化或其它方面的指導,這些Attribute,是不是使得領域模型變得不夠純淨了?象上面的[AggregateRoot(“Customers”)],至少從感覺上覺得,它跟領域模型或者說業務領域一點關係都沒有。如果要得到更純淨的領域模型,是不是可以加一層領域模型與數據持久模型的映射,這樣會不會好點?
以下爲blog主人的回覆: |
[ 2010-2-26 16:03:00 | By: haojie77 ] "EdmRepository將實體框架抽象到ObjectContext這一層,這也使我們沒法通過LINQ to Entities來查詢模型中的對象." 從現在的實現方式看, 擁有ObjectContext的CustomerRepository是否被一直緩存起來了? 這是否意味着數據庫的鏈接始終保持着? 目前的EdmRepository只能進行聚合根的CRUD操作, 如果要獲得聚合中的其他實體(勢必要使用linq to entity的操作, 若不對請加以指正), 要在哪裏實現呢? 是否在EdmRepository中追加定義一些specification的查詢? (在<<NET.Domain.Driven.Design.with.C#>>中聚合中的其他實體也是從該聚合根的 Repository來獲得.) 非常期待您之後的講座!
以下爲blog主人的回覆: |
[ 2010-2-28 0:14:00 | By: ruson(遊客) ]
按您文中所述,領域模型中用到了倉儲,這裏項目如果分開的話就會有相互引用,因爲倉庫實現中會用到領域Domain。雖然用依賴注入可以解決。但覺得這本身已經不太合理了。
以下爲blog主人的回覆: |
[ 2010-2-28 0:21:00 | By: ruson(遊客) ] 沒有最好的架構,只有最合理的架構。當系統不復雜且大部分都是對單一表的CURD操作時,領域驅動就變得像貧血模型,感覺是個雞肋。
以下爲blog主人的回覆: |
[ 2010-3-1 16:10:00 | By: haojie77 ]
您說的這個Address只是一個Customer的屬性. 我想知道的是如何得到某個特殊的在Customer聚合中的實體.
以下爲 blog主人的回覆:
class Customer |