一、前言
在前面專題一中,我已經介紹了我寫這系列文章的初衷了。由於dax.net中的DDD框架和Byteart Retail案例並沒有對其形成過程做一步步分析,而是把整個DDD的實現案例展現給我們,這對於一些剛剛接觸領域驅動設計的朋友可能會非常迷茫,從而覺得領域驅動設計很難,很複雜,因為學習中要消化一個整個案例的知識,這樣未免很多人消化不了就打退堂鼓,就不繼續研究下去了,所以這樣也不利於DDD的推廣。然而本系列可以說是剛接觸領域驅動設計朋友的福音,本系列將結合領域驅動設計的思想來一步步構建一個網上書店,從而讓大家學習DDD不再枯燥和可以看到一個DDD案例的形成歷程。最後,再DDD案例完成之後,將從中抽取一個領域驅動的框架,從而大家也可以看到一個DDD框架的形成歷程,這樣就不至於一下子消化一整個框架和案例的知識,而是一步步消化。接下來,該專題將介紹的是:結合領域驅動設計的SOA架構來構建網上書店,本專題中並沒有完成網上書店的所有頁面和覆蓋DDD中的所有內容,而只是一部分,後面的專題將會在本專題的網上書店進行一步步完善,通過一步步引入DDD的內容和重構來完成整個專案。
二、DDD分層架構
從概念上說,領域驅動設計架構主要分為四層,分別為:基礎設施層、領域層、應用層和表現層。
- 基礎結構層:該層專為其他各層提供各項通用技術框架支援。像一些配置檔案處理、快取處理,事務處理等都可以放在這裡。
- 領域層:簡單地說就是業務所涉及的領域物件(包括實體、值物件)、領域服務等。該層就是所謂的領域模型了,領域驅動設計提倡是富領域模型,富領域模型指的是:儘量將業務邏輯放在歸屬於它的領域物件中。而之前的三層架構中的領域模型都是貧血領域模型,因為在三層中的領域模型只包含業務屬性,而不包含任何業務邏輯。本專題的網上書店領域模型目前還沒有包含任何業務邏輯,在後期將會完善。
實體可以認為對應於資料庫的表,而值物件一般定義在實體類中。
- 應用層:該層不包含任何領域邏輯,它主要用來對任務進行協調,它構建了表現層和領域層的橋樑。SOA架構就是在該層進行實現的。
- 表現層:指的是使用者介面,例如Asp.net mvc網站,WPF、Winform和控制檯等。它主要用來想使用者展現內容。
下面用一個圖來形象展示DDD的分層架構:
本系列介紹的領域驅動設計實戰,則自然少了領域驅動設計分層架構的實現了,上面簡單介紹了領域驅動的分層架構,接下來將詳細介紹在網上書店中各層是如何去實現的。
三、網上書店領域模型層的實現
在應用領域驅動設計的思想來構建一個專案,則第一步就是了解需求,明白專案的業務邏輯,瞭解清楚業務邏輯後,則把業務邏輯抽象成領域物件,領域物件所放在的位置也就是領域模型層了。該專題介紹的網上書店主要完成了商品所涉及的頁面,包括商品首頁,單個商品的詳細資訊等。所以這裡涉及的領域實體包括2個,一個是商品類,另外一個就是類別類,因為在商品首頁中,需要顯示所有商品的類別。在給出領域物件的實現之前,這裡需要介紹領域層中所涉及的幾個概念。
- 聚合根:聚合根也是實體,但與實體不同的是,聚合根是由實體和值物件組成的系統邊界物件。舉個例子來說,例如訂單和訂單項,根據業務邏輯,我們需要跟蹤訂單和訂單項的狀態,所以設計它們都為實體,但只有訂單才是聚合根物件,而訂單項不是,因為訂單項只有在訂單中才有意義,意思就是說:使用者不能直接看到訂單項,而是先查詢到訂單,然後再看到該訂單下的訂單項。所以聚合根可以理解為使用者直接操作的物件。在這裡商品類和類別類都是一個聚合根。
根據面向介面程式設計原則,我們在領域模型中應該定義一個實體介面和聚合根介面,而因為聚合根也是屬於實體,所以聚合根介面繼承於實體介面,而商品類和類別類都是聚合根,所以它們都實現聚合根介面。如果像訂單項只是實體不是聚合根的類則實現實體介面。有了上面的分析,則領域模型層的實現也就自然出來了,下面是領域物件的具體實現:
// 領域實體介面 public interface IEntity { // 當前領域實體的全域性唯一標識 Guid Id { get; } }
// 聚合根介面,繼承於該介面的物件是外部唯一操作的物件 public interface IAggregateRoot : IEntity { }
// 商品類 public class Product : AggregateRoot { public string Name { get; set; } public string Description { get; set; } public decimal UnitPrice { get; set; } public string ImageUrl { get; set; } public bool IsNew{ get; set; } public override string ToString() { return Name; } }
// 類別類 public class Category : AggregateRoot { public string Name { get; set; } public string Description { get; set; } public override string ToString() { return this.Name; } }
另外,領域層除了實現領域物件外,還需要定義倉儲介面,而倉儲層則是對倉儲介面的實現。倉儲可以理解為在記憶體中維護一系列聚合根的集合,而聚合根不可能一直存在於記憶體中,當它不活動時會被持久化到資料中。而倉儲層完成的任務是持久化聚合根物件到資料或從資料庫中查詢儲存的物件來重新建立領域物件。
倉儲層有幾個需要明確的概念:
- 倉儲裡面存放的物件一定是聚合根,因為領域模型是以聚合根的概念去劃分的,聚合根就是我們操作物件的一個邊界。所以我們都是對某個聚合根進行操作的,而不存在對聚合內的值物件進行操作。因此,倉儲只針對聚合根設計。
- 因為倉儲只針對聚合根設計,所以一個聚合根需要實現一個倉儲。
- 不要把倉儲簡單理解為DAO,倉儲屬於領域模型的一部分,代表了領域模型向外界提供介面的一部分,而DAO是表示資料庫向上層提供的介面表示。一個是針對領域模型而言,而另一個針對資料庫而言。兩者側重點不一樣。
- 倉儲分為定義部分和實現部分,在領域模型中定義倉儲的介面,而在基礎設施層實現具體的倉儲。這樣做的原因是:由於倉儲背後的實現都是在和資料庫打交道,但是我們又不希望客戶(如應用層)把重點放在如何從資料庫獲取資料的問題上,因為這樣做會導致客戶(應用層)程式碼很混亂,很可能會因此而忽略了領域模型的存在。所以我們需要提供一個簡單明瞭的介面,供客戶使用,確保客戶能以最簡單的方式獲取領域物件,從而可以讓它專心的不會被什麼資料訪問程式碼打擾的情況下協調領域物件完成業務邏輯。這種通過介面來隔離封裝變化的做法其實很常見。由於客戶面對的是抽象的介面並不是具體的實現,所以我們可以隨時替換倉儲的真實實現,這很有助於我們做單元測試。在本專題的案例中,我們把倉儲層的實現單獨從基礎設施層拎出來了,作為一個獨立的層存在。這也就是為什麼DDD分層中沒有定義倉儲層啊,而本專題的案例中多了一個倉儲層的實現。
- 倉儲在設計查詢介面時,會經常用到規約模式(Specification Pattern)。本專題的案例中沒有給出,這點將會在後面專題新增上去。
- 倉儲一般不負責事務處理,一般事務處理會交給“工作單元(Unit Of Work)”去處理,同樣本專題也沒有涉及工作單元的實現,這點同樣會在後面專題繼續完善。這裡列出來讓大家對後面的專題可以有個清晰的概念,而不至於是空穴來風的。
介紹完倉儲之後,接下來就在領域層中定義倉儲介面,因為本專題中涉及到2個聚合根,則自然需要實現2個倉儲介面。根據面向介面程式設計原則,我們讓這2個倉儲介面都實現與一個公共的介面:IRepository介面。另外倉儲介面還需要定義一個倉儲上下介面,因為在Entity Framework中有一個DbContex類,所以我們需要定義一個EntityFramework上下文物件來對DbContex進行包裝。也就自然有了倉儲上下文介面了。經過上面的分析,倉儲介面的實現也就一目瞭然了。
// 倉儲介面 public interface IRepository<TAggregateRoot> where TAggregateRoot :class, IAggregateRoot { void Add(TAggregateRoot aggregateRoot); IEnumerable<TAggregateRoot> GetAll(); // 根據聚合根的ID值,從倉儲中讀取聚合根 TAggregateRoot GetByKey(Guid key); }
public interface IProductRepository : IRepository<Product> { IEnumerable<Product> GetNewProducts(int count = 0); }
public interface IProductRepository : IRepository<Product> { IEnumerable<Product> GetNewProducts(int count = 0); }
// 倉儲上下文介面 public interface IRepositoryContext { }
這樣我們就完成了領域層的搭建了,接下面,我們就需要對領域層中定義的倉儲介面進行實現了。我這裡將倉儲介面的實現單獨弄出了一個層,當然你也可以放在基礎設施層中的Repositories資料夾中。不過我看很多人都直接拎出來的。我這裡也是直接作為一個層。
四、網上書店Repository(倉儲)層的實現
定義完倉儲介面之後,接下來就是在倉儲層實現這些介面,完成領域物件的序列化。首先是產品倉儲的實現:
// 商品倉儲的實現 public class ProductRepository : IProductRepository { #region Private Fields private readonly IEntityFrameworkRepositoryContext _efContext; #endregion #region Public Properties public IEntityFrameworkRepositoryContext EfContext { get { return this._efContext; } } #endregion #region Ctor public ProductRepository(IRepositoryContext context) { var efContext = context as IEntityFrameworkRepositoryContext; if (efContext != null) this._efContext = efContext; } #endregion public IEnumerable<Product> GetNewProducts(int count = 0) { var ctx = this.EfContext.DbContex as OnlineStoreDbContext; if (ctx == null) return null; var query = from p in ctx.Products where p.IsNew == true select p; if (count == 0) return query.ToList(); else return query.Take(count).ToList(); } public void Add(Product aggregateRoot) { throw new NotImplementedException(); } public IEnumerable<Product> GetAll() { var ctx = this.EfContext.DbContex as OnlineStoreDbContext; if (ctx == null) return null; var query = from p in ctx.Products select p; return query.ToList(); } public Product GetByKey(Guid key) { return EfContext.DbContex.Products.First(p => p.Id == key); } }
接下來是類別倉儲的實現:
// 類別倉儲的實現 public class CategoryRepository :ICategoryRepository { #region Private Fields private readonly IEntityFrameworkRepositoryContext _efContext; public CategoryRepository(IRepositoryContext context) { var efContext = context as IEntityFrameworkRepositoryContext; if (efContext != null) this._efContext = efContext; } #endregion #region Public Properties public IEntityFrameworkRepositoryContext EfContext { get { return this._efContext; } } #endregion public void Add(Category aggregateRoot) { throw new System.NotImplementedException(); } public IEnumerable<Category> GetAll() { var ctx = this.EfContext.DbContex as OnlineStoreDbContext; if (ctx == null) return null; var query = from c in ctx.Categories select c; return query.ToList(); } public Category GetByKey(Guid key) { return this.EfContext.DbContex.Categories.First(c => c.Id == key); } }
由於後期除了實現基於EF倉儲的實現外,還想實現基於MongoDb倉儲的實現,所以在倉儲層中建立了一個EntityFramework的資料夾,並定義了一個IEntityFrameworkRepositoryContext介面來繼承於IRepositoryContext介面,由於EF中持久化資料主要是由DbContext物件來完成了,為了有自己框架模型,我在這裡定義了OnlineStoreDbContext來繼承DbContext,從而用OnlineStoreDbContext來對DbContext進行了一次包裝。經過上面的分析之後,接下來對於實現也就一目瞭然了。首先是OnlineStoreDbContext類的實現:
public sealed class OnlineStoreDbContext : DbContext { #region Ctor public OnlineStoreDbContext() : base("OnlineStore") { this.Configuration.AutoDetectChangesEnabled = true; this.Configuration.LazyLoadingEnabled = true; } #endregion #region Public Properties public DbSet<Product> Products { get { return this.Set<Product>(); } } public DbSet<Category> Categories { get { return this.Set<Category>(); } } // 後面會繼續新增屬性,針對每個聚合根都會定義一個DbSet的屬性 // ... #endregion }
接下來就是IEntityFrameworkRepositoryContext介面的定義以及它的實現了。具體程式碼如下所示:
public interface IEntityFrameworkRepositoryContext : IRepositoryContext { #region Properties OnlineStoreDbContext DbContex { get; } #endregion }
public class EntityFrameworkRepositoryContext : IEntityFrameworkRepositoryContext { // 引用我們定義的OnlineStoreDbContext類物件 public OnlineStoreDbContext DbContex { get { return new OnlineStoreDbContext(); } } }
這樣,我們的倉儲層也就完成了。接下來就是應用層的實現。
五、網上書店應用層的實現
應用層應用了面向服務結構進行實現,採用了微軟面向服務的實現WCF來完成的。網上書店的整個架構完全遵循著領域驅動設計的分層架構,使用者通過UI層(這裡實現的是Web頁面)來進行操作,然後UI層呼叫應用層來把服務進行分發,通過呼叫基礎設施層中倉儲實現來對領域物件進行持久化和重建。這裡應用層主要採用WCF來實現的,其中引用了倉儲介面。針對服務而言,首先就需要定義服務契約了,這裡我把服務契約的定義單獨放在了一個服務契約層,當然你也可以在應用層中建立一個服務契約資料夾。首先就去看看服務契約的定義:
// 商品服務契約的定義 [ServiceContract(Namespace="")] public interface IProductService { #region Methods // 獲得所有商品的契約方法 [OperationContract] IEnumerable<Product> GetProducts(); // 獲得新上市的商品的契約方法 [OperationContract] IEnumerable<Product> GetNewProducts(int count); // 獲得所有類別的契約方法 [OperationContract] IEnumerable<Category> GetCategories(); // 根據商品Id來獲得商品的契約方法 [OperationContract] Product GetProductById(Guid id); #endregion }
接下來就是服務契約的實現,服務契約的實現我放在應用層中,具體的實現程式碼如下所示:
// 商品服務的實現 public class ProductServiceImp : IProductService { #region Private Fields private readonly IProductRepository _productRepository; private readonly ICategoryRepository _categoryRepository; #endregion #region Ctor public ProductServiceImp(IProductRepository productRepository, ICategoryRepository categoryRepository) { _categoryRepository = categoryRepository; _productRepository = productRepository; } #endregion #region IProductService Members public IEnumerable<Product> GetProducts() { return _productRepository.GetAll(); } public IEnumerable<Product> GetNewProducts(int count) { return _productRepository.GetNewProducts(count); } public IEnumerable<Category> GetCategories() { return _categoryRepository.GetAll(); } public Product GetProductById(Guid id) { var product = _productRepository.GetByKey(id); return product; } #endregion }
最後就是建立WCF服務來呼叫服務契約實現了。建立一個字尾為.svc的WCF服務檔案,WCF服務的具體實現如下所示:
// 商品WCF服務 public class ProductService : IProductService { // 引用商品服務介面 private readonly IProductService _productService; public ProductService() { _productService = ServiceLocator.Instance.GetService<IProductService>(); } public IEnumerable<Product> GetProducts() { return _productService.GetProducts(); } public IEnumerable<Product> GetNewProducts(int count) { return _productService.GetNewProducts(count); } public IEnumerable<Category> GetCategories() { return _productService.GetCategories(); } public Product GetProductById(Guid id) { return _productService.GetProductById(id); } }
到這裡我們就完成了應用層面向服務架構的實現了。從商品的WCF服務實現可以看到,我們有一個ServiceLocator的類。這個類的實現採用服務定位器模式,關於該模式的介紹可以參考dax.net的服務定位器模式的介紹。該類的作用就是呼叫方具體的例項,簡單地說就是通過服務介面定義具體服務介面的實現,將該實現返回給呼叫者的。這個類我這裡放在了基礎設施層來實現。目前基礎設施層只有這一個類的實現,後期會繼續新增其他功能,例如快取功能的支援。
另外,在這裡使用了Unity依賴注入容器來對介面進行注入。主要的配置檔案如下所示:
<configuration> <configSections> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework"/> <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/> </configSections> <!-- Entity Framework 配置資訊--> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework"> <parameters> <parameter value="Data Source=(LocalDb)\v11.0; Initial Catalog=OnlineStore; Integrated Security=True; Connect Timeout=120; MultipleActiveResultSets=True; AttachDBFilename=|DataDirectory|\OnlineStore.mdf"/> </parameters> </defaultConnectionFactory> </entityFramework> <!--Unity的配置資訊--> <unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <container> <!--倉儲介面的註冊--> <register type="OnlineStore.Domain.Repositories.IRepositoryContext, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.EntityFrameworkRepositoryContext, OnlineStore.Repositories"/> <register type="OnlineStore.Domain.Repositories.IProductRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.ProductRepository, OnlineStore.Repositories"/> <register type="OnlineStore.Domain.Repositories.ICategoryRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.CategoryRepository, OnlineStore.Repositories"/> <!--應用服務的註冊--> <register type="OnlineStore.ServiceContracts.IProductService, OnlineStore.ServiceContracts" mapTo="OnlineStore.Application.ServiceImplementations.ProductServiceImp, OnlineStore.Application"/> </container> </unity> <appSettings> <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true"/> </appSettings> <system.web> <compilation debug="true" targetFramework="4.5"/> <httpRuntime targetFramework="4.5.1"/> </system.web> <!--WCF 服務的配置資訊--> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior name=""> <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/> <serviceDebug includeExceptionDetailInFaults="true"/> </behavior> </serviceBehaviors> </behaviors> <services> <service name="OnlineStore.Application.ServiceImplementations.ProductServiceImp" behaviorConfiguration=""> <endpoint address="" binding="wsHttpBinding" contract="OnlineStore.ServiceContracts.IProductService"/> <!--<endpoint contract="IMetadataExchange" binding="mexHttpBinding" address="mex" />--> </service> </services> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true"/> </system.serviceModel> <system.webServer> <modules runAllManagedModulesForAllRequests="true"/> <!-- To browse web app root directory during debugging, set the value below to true. Set to false before deployment to avoid disclosing web app folder information. --> <directoryBrowse enabled="true"/> </system.webServer> </configuration>
六、基礎設施層的實現
基礎設施層在本專題中只包含了服務定位器的實現,後期會繼續新增對其他功能的支援,ServiceLocator類的具體實現如下所示:
// 服務定位器的實現 public class ServiceLocator : IServiceProvider { private readonly IUnityContainer _container; private static ServiceLocator _instance = new ServiceLocator(); private ServiceLocator() { _container = new UnityContainer(); _container.LoadConfiguration(); } public static ServiceLocator Instance { get { return _instance; } } #region Public Methods public T GetService<T>() { return _container.Resolve<T>(); } public IEnumerable<T> ResolveAll<T>() { return _container.ResolveAll<T>(); } public T GetService<T>(object overridedArguments) { var overrides = GetParameterOverrides(overridedArguments); return _container.Resolve<T>(overrides.ToArray()); } public object GetService(Type serviceType, object overridedArguments) { var overrides = GetParameterOverrides(overridedArguments); return _container.Resolve(serviceType, overrides.ToArray()); } #endregion #region Private Methods private IEnumerable<ParameterOverride> GetParameterOverrides(object overridedArguments) { var overrides = new List<ParameterOverride>(); var argumentsType = overridedArguments.GetType(); argumentsType.GetProperties(BindingFlags.Public | BindingFlags.Instance) .ToList() .ForEach(property => { var propertyValue = property.GetValue(overridedArguments, null); var propertyName = property.Name; overrides.Add(new ParameterOverride(propertyName, propertyValue)); }); return overrides; } #endregion #region IServiceProvider Members public object GetService(Type serviceType) { return _container.Resolve(serviceType); } #endregion }
七、UI層的實現
根據領域驅動的分層架構,接下來自然就是UI層的實現了,這裡UI層的實現採用Asp.net MVC 技術來實現的。UI層主要包括商品首頁的實現,和詳細商品的實現,另外還有一些附加頁面的實現,例如,關於頁面,聯絡我們頁面等。關於UI層的實現,這裡就不一一貼出程式碼實現了,大家可以在最後的原始碼連結自行下載檢視。
八、系統總體架構
經過上面的所有步驟,本專題中的網上書店構建工作就基本完成了,接下來我們來看看網上書店的總體架構圖(這裡架構圖直接借鑑了dax.net的圖了,因為本系列文章也是對其Byteart Retail專案的剖析過程):
最後附上整個解決方案的結構圖:
九、網上書店執行效果
實現完之後,大家是不是都已經迫不及待地想看到網上書店的執行效果呢?下面就為大家來揭曉,目前網上書店主要包括2個頁面,一個是商品首頁的展示和商品詳細資訊的展示。首先看下商品首頁的樣子吧:
圖書的詳細資訊頁面:
十、總結
到這裡,本專題的內容就介紹完了, 本專題主要介紹面向領域驅動設計的分層架構和麵向服務架構。然後結合它們在網上書店中進行實戰演練。在後面的專題中我會在該專案中一直進行完善,從而形成一個完整了DDD案例。在接下來的專題會對倉儲的實現應用規約模式,在應用之前,我會先寫一個專題來介紹規約模式來作為一個準備工作。
GitHub 開源地址:https://github.com/lizhi5753186/OnlineStore。