规 约(Specification
)模式:第一次看到这东西是在microsoft NLayer
项目中,它是微软对DDD
的解说,就像petshop
告诉了我们MVC如何使用一样,这个规约模式最重要的作用是实现了查询语句与查询条件的 分离,查询语句在底层是稳定的,不变的,而查询条件是和具体业务,具体领域有关的,是易变的,如果我们为每一个领域的每一个新需求都写一个新的方法,那就 会出现很多重复的代码,不利于程序的最终扩展!
下面我们来看一个经典例子
一个IOrderRepository
的接口,定义了一个订单仓储
Order_Info GetOrder_InfoById(int orderID);
List<Order_Info> GetOrder_Info(DateTime from, DateTime to);
List<Order_Info> GetOrder_InfoByUser(int userID);
代码本身没有任何问题,你只要去实现它就可以了,当一个新的需求到了之后,你的接口要被扩展(这是不被提倡的,一般我们会新建一个接口),然后修改
原来的实现类,去实现接口新的方法(违背了OCP
原则),这种做法是大多部开发团队所经历了,我,一个普通的人,也经历了,但当我知道DDD后,当我看完microsoft Nlayer
项目之后,我知道,我一定要改变这种局面,于是,代码在规约模式的指导下,进行重构了,呵呵。
先看一下规约模式的类关系图
下面是我对原来结构的修改(由于原程序是三层架构,所以我就不改变原有架构了,只是对代码进行重构,DAL层,BLL层,WEB层
)
基础设施层
IRepository
仓储接口如下,怎么去实现就不放了,呵呵
public interface IRepository<TEntity>
where TEntity : class
{
/// <summary>
/// 添加实体并提交到数据服务器
/// </summary>
/// <param name="item">Item to add to repository</param>
void Add(TEntity item);
/// <summary>
/// 移除实体并提交到数据服务器
/// 如果表存在约束,需要先删除子表信息
/// </summary>
/// <param name="item">Item to delete</param>
void Remove(TEntity item);
/// <summary>
/// 修改实体并提交到数据服务器
/// </summary>
/// <param name="item"></param>
void Modify(TEntity item);
/// <summary>
/// 通过指定规约,得到实体对象
/// </summary>
/// <param name="specification"></param>
/// <returns></returns>
TEntity GetEntity(ISpecification<TEntity> specification);
/// <summary>
/// 通用表达式树,得到实体
/// </summary>
/// <param name="predicate"></param>
/// <returns></returns>
TEntity GetEntity(Expression<Func<TEntity, bool>> predicate);
/// <summary>
/// Get all elements of type {T} in repository
/// </summary>
/// <returns>List of selected elements</returns>
IQueryable<TEntity> GetEntities();
/// <summary>
/// Get all elements of type {T} that matching a
/// Specification <paramref name="specification"/>
/// </summary>
/// <param name="specification">Specification that result meet</param>
/// <returns></returns>
IQueryable<TEntity> GetEntities(ISpecification<TEntity> specification);
/// <summary>
/// 通用表达式树,得到集合
/// </summary>
/// <param name="predicate"></param>
/// <returns></returns>
IQueryable<TEntity> GetEntities(Expression<Func<TEntity, bool>> predicate);
}
IOrderRepository
接口如下
public interface IOrderRepository :Domain.Core.IRepository<Order_Info>
{
void InsertOrder(Order_Info entity);
}
DAL
底层为数据持久化层,它是非常稳定的,只提供最基本的表操作,具体业务如何组成,全放在BLL
层去实现
领域层
这一层中定义具体业务的规约,并组成查询方法及调用DAL
层的具体方法(DAL
层来接受从BLL
层传过来的ISpecification
参数)
/// <summary>
/// 根据下单日期得到订单列表
/// </summary>
public class OrderFromDateSpecification : Specification<Order_Info>
{
DateTime? _fromDate;
DateTime? _toDate;
public OrderFromDateSpecification(DateTime? fromDate, DateTime? toDate)
{
_fromDate = fromDate ?? DateTime.MinValue;
_toDate = toDate ?? DateTime.MaxValue;
}
public override global::System.Linq.Expressions.Expression<Func<Order_Info, bool>> SatisfiedBy()
{
Specification<Order_Info> spec = new TrueSpecification<Order_Info>();
spec &= new DirectSpecification<Order_Info>(o => o.CreateDate >= _fromDate
&& o.CreateDate <= _toDate);
return spec.SatisfiedBy();
}
}
/// <summary>
/// 通过用户信息得到他的订单列表
/// </summary>
public class OrderFromUserSpecification : Specification<Order_Info>
{
int _userID = default(Int32);
public OrderFromUserSpecification(int userID)
{
_userID = userID;
}
public override global::System.Linq.Expressions.Expression<Func<Order_Info, bool>> SatisfiedBy()
{
Specification<Order_Info> spec = new TrueSpecification<Order_Info>();
spec &= new DirectSpecification<Order_Info>(o => o.UserID == _userID);
return spec.SatisfiedBy();
}
}
业务层真实的查询主体,只要在一个方法里写就OK了,然后它非常稳定,如果以后还有其它查询业务出来,直接添加一个查询规约即可
/// <summary>
/// 根据WEB层传来及组件好的规约,返回集体
/// </summary>
/// <param name="spec"></param>
/// <returns></returns>
public List<Order_Info> GetOrder_InfoBySpec(ISpecification<Order_Info> spec)
{
return _iOrderRepository.GetEntities(spec).ToList();
}
WEB层
Web
层建立一个指定的规约,并为规约组件所需要的数据即可
public ActionResult List(int? userID)
{
ISpecification<Order_Info> spec = new OrderFromUserSpecification(userID ?? 0);
var model = orderService.GetOrder_InfoBySpec(spec);
return View(model);
}
如果这时来了个新需要,使用用户名进行查询,你可以直接建立一个OrderFromUserNameSpecification
的规约即可,而不需要修改OrderService
,呵呵!
上一讲介绍了DDD
中的领域层,并提到下次要讲Unity
,所以这篇文章当然就要介绍它了,呵呵,Unity
是Microsoft.Practices
中的一部分,主要实现了依赖注入的功能,或者叫它控制反转,对于控制反转(IoC
)的文章我介绍了不少,Autofac,Castle
等等,今天主要说一下Unity
!
在我的DDD
架构项目中,各层间实现IoC
使用的是Unity
,因为考虑到AOP,cache
等功能,所以就直接用Microsoft.Practices
组件了,它真的很强大!这次的项目在业务上采用WCF
实现,所以WCF
业务与基础设施之间会有通信,而基础设施只是去实现Domain
定义的功能,所以这两个层之间也会有通信,最后就是Domain
与WCF
之间同样存在着接口的通信,如图:
Domain
层定义一个接口,部分代码如下:
/// <summary>
/// 获取产品列表
/// </summary>
/// <returns></returns>
IQueryable<Product> GetProduct();
基础设施层实现它
public IQueryable<Product> GetProduct()
{
return this.GetModel();
}
它们之间的通信不存在IoC
,因为如果你要使用其它的持久化方法,可以再建立一个项目,以别一种方式去实现持久化
WCF
去声明一个Domain
层的接口,实例化它的基础设施层的实例,这个过程需要解耦合,我们使用Unity
来实现,它需要我们在config
中去定义如何去实例化。
public class ProductServiceImpl : IProductService
{
private readonly IProductRepository _productRepository;
/// <summary>
/// 构造方法注入
/// </summary>
/// <param name="productRepository"></param>
public ProductServiceImpl(IProductRepository productRepository)
{
if (productRepository == (IProductRepository)null)
throw new ArgumentException("context can't is null.");
_productRepository = productRepository;
}
#region IProductService 成员
public ProductDTO GetProductByID(int id)
{
Mapper.CreateMap<Product, ProductDTO>();
return Mapper.Map<Product, ProductDTO>(_productRepository.Find(id));
}
}
上面使用unity
中的构造方法注入,我们还可以使用服务调度器进行注入,看这种代码
public class ProductService : ServiceBase, IProductService
{
//通过ServiceLocator从IoC容器中获得对象
IProductService _productService = ServiceLocator.Instance.GetService<IProductService>();
#region IProductService 成员
public ProductDTO GetProductByID(int id)
{
return _productService.GetProductByID(id);
}
}
下面是配置文件中需要注入的代码片断,包括缓存模块和日志模块
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration" />
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
<section name="cachingConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Caching.Configuration.CacheManagerSettings, Microsoft.Practices.EnterpriseLibrary.Caching, Version=5.0.505.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</configSections>
<!--BEGIN: Unity-->
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration" />
<container>
<extension type="Interception" />
<register type="Project.Caching.ICacheProvider, Project.Caching" mapTo="Project.Caching.EntLibCacheProvider, Project.Caching" />
<!--对数据上下文的注入-->
<register type="DDD_AOP_WCF.Domain.Products.IProductRepository, DDD_AOP_WCF.Domain" mapTo="DDD_AOP_WCF.Infrastructure.Repository.ProductRepository, DDD_AOP_WCF.Infrastructure" />
<!--对WCF的访问进行的注入与缓存和异常的拦截-->
<register type="DDD_AOP_WCF.ServiceContracts.IProductService, DDD_AOP_WCF.ServiceContracts" mapTo="DDD_AOP_WCF.Service.Implements.ProductServiceImpl, DDD_AOP_WCF.Service">
<!-- <interceptor type="VirtualMethodInterceptor" />-->
<interceptor type="InterfaceInterceptor" />
<interceptionBehavior type="Project.InterceptionBehaviors.CachingBehavior, Project.InterceptionBehaviors" />
<interceptionBehavior type="Project.InterceptionBehaviors.ExceptionLoggingBehavior, Project.InterceptionBehaviors" />
</register>
</container>
</unity>
<!--END: Unity-->
<!--BEGIN: Caching-->
<cachingConfiguration defaultCacheManager="ByteartRetailCacheManager">
<cacheManagers>
<add name="ByteartRetailCacheManager" type="Microsoft.Practices.EnterpriseLibrary.Caching.CacheManager, Microsoft.Practices.EnterpriseLibrary.Caching, Version=5.0.505.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" expirationPollFrequencyInSeconds="600" maximumElementsInCacheBeforeScavenging="1000" numberToRemoveWhenScavenging="10" backingStoreName="NullBackingStore" />
</cacheManagers>
<backingStores>
<add type="Microsoft.Practices.EnterpriseLibrary.Caching.BackingStoreImplementations.NullBackingStore, Microsoft.Practices.EnterpriseLibrary.Caching, Version=5.0.505.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" name="NullBackingStore" />
</backingStores>
</cachingConfiguration>
<!--END: Caching-->
<!--BEGIN: log4net-->
<log4net>
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="log.log"/>
<appendToFile value="true"/>
<rollingStyle value="Size"/>
<maxSizeRollBackups value="10"/>
<maximumFileSize value="100KB"/>
<staticLogFileName value="true"/>
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="%newline%date [%thread] %-5level %logger - %message%newline"/>
</layout>
</appender>
<logger name="ByteartRetail.Logger" >
<!--control log level: ALL|DEBUG|INFO|WARN|ERROR|FATAL|OFF-->
<!--如果没有定义LEVEL的值,则缺省为DEBUG-->
<level value="INFO"/>
<appender-ref ref="RollingFileAppender"/>
</logger>
</log4net>
<!--END: log4net-->