在前面专题一中,我已经介绍了我写这系列文章的初衷了。由于dax.net中的DDD框架和Byteart Retail案例并没有对其形成过程做一步步分析,而是把整个DDD的实现案例展现给我们,这对于一些刚刚接触领域驱动设计的朋友可能会非常迷茫,从而觉得领域驱动设计很难,很复杂,因为学习中要消化一个整个案例的知识,这样未免很多人消化不了就打退堂鼓,就不继续研究下去了,所以这样也不利于DDD的推广。然而本系列可以说是刚接触领域驱动设计朋友的福音,本系列将结合领域驱动设计的思想来一步步构建一个网上书店,从而让大家学习DDD不再枯燥和可以看到一个DDD案例的形成历程。最后,再DDD案例完成之后,将从中抽取一个领域驱动的框架,从而大家也可以看到一个DDD框架的形成历程,这样就不至于一下子消化一整个框架和案例的知识,而是一步步消化。接下来,该专题将介绍的是:结合领域驱动设计的SOA架构来构建网上书店,本专题中并没有完成网上书店的所有页面和覆盖DDD中的所有内容,而只是一部分,后面的专题将会在本专题的网上书店进行一步步完善,通过一步步引入DDD的内容和重构来完成整个项目。
从概念上说,领域驱动设计架构主要分为四层,分别为:基础设施层、领域层、应用层和表现层。
下面用一个图来形象展示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 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; } }
另外,领域层除了实现领域对象外,还需要定义仓储接口,而仓储层则是对仓储接口的实现。仓储可以理解为在内存中维护一系列聚合根的集合,而聚合根不可能一直存在于内存中,当它不活动时会被持久化到数据中。而仓储层完成的任务是持久化聚合根对象到数据或从数据库中查询存储的对象来重新创建领域对象。
仓储层有几个需要明确的概念:
介绍完仓储之后,接下来就在领域层中定义仓储接口,因为本专题中涉及到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文件夹中。不过我看很多人都直接拎出来的。我这里也是直接作为一个层。
定义完仓储接口之后,接下来就是在仓储层实现这些接口,完成领域对象的序列化。首先是产品仓储的实现:
// 商品仓储的实现 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); } }View Code
接下来是类别仓储的实现:
// 类别仓储的实现 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); } }View Code
由于后期除了实现基于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 }View Code
接下来就是服务契约的实现,服务契约的实现我放在应用层中,具体的实现代码如下所示:
// 商品服务的实现 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 }View Code
最后就是创建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的类。这个类的实现采用服务定位器模式,关于该模式的介绍可以参考d ax.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>View Code
基础设施层在本专题中只包含了服务定位器的实现,后期会继续添加对其他功能的支持,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 }View Code
根据领域驱动的分层架构,接下来自然就是UI层的实现了,这里UI层的实现采用Asp.net MVC 技术来实现的。UI层主要包括商品首页的实现,和详细商品的实现,另外还有一些附加页面的实现,例如,关于页面,联系我们页面等。关于UI层的实现,这里就不一一贴出代码实现了,大家可以在最后的源码链接自行下载查看。
经过上面的所有步骤,本专题中的网上书店构建工作就基本完成了,接下来我们来看看网上书店的总体架构图(这里架构图直接借鉴了dax.net的图了,因为本系列文章也是对其Byteart Retail项目的剖析过程):
最后附上整个解决方案的结构图:
实现完之后,大家是不是都已经迫不及待地想看到网上书店的运行效果呢?下面就为大家来揭晓,目前网上书店主要包括2个页面,一个是商品首页的展示和商品详细信息的展示。首先看下商品首页的样子吧:
图书的详细信息页面:
到这里,本专题的内容就介绍完了, 本专题主要介绍面向领域驱动设计的分层架构和面向服务架构。然后结合它们在网上书店中进行实战演练。在后面的专题中我会在该项目中一直进行完善,从而形成一个完整了DDD案例。在接下来的专题会对仓储的实现应用规约模式,在应用之前,我会先写一个专题来介绍规约模式来作为一个准备工作。
GitHub 开源地址: https://github.com/lizhi5753186/OnlineStore 。