前言:上篇介绍了DDD设计Demo里面的聚合划分以及实体和聚合根的设计,这章继续来说说DDD里面最具争议的话题之一的仓储Repository,为什么Repository会有这么大的争议,博主认为主要原因无非以下两点:一是Repository的真实意图没有理解清楚,导致设计的紊乱,随着项目的横向和纵向扩展,到最后越来越难维护;二是赶时髦的为了“模式”而“模式”,仓储并非适用于所有项目,这就像没有任何一种架构能解决所有的设计难题一样。本篇通过这个设计的Demo来谈谈博主对仓储的理解,有不对的地方还望园友们斧正!

DDD领域驱动设计初探系列文章:

一、仓储的定义

仓储,顾名思义,存储数据的仓库。那么有人就疑惑了,既然我们有了数据库来存取数据,为什么还要弄一个仓储的概念,其实博主觉得这是一个考虑层面不同的问题,数据库主要用于存取数据,而仓储作用之一是用于数据的持久化。从架构层面来说,仓储用于连接领域层和基础结构层,领域层通过仓储访问存储机制,而不用过于关心存储机制的具体细节。按照DDD设计原则,仓储的作用对象的领域模型的聚合根,也就是说每一个聚合都有一个单独的仓储。可能这样说大家未必能理解,相信看了仓储的代码设计,大家能有一个更加透彻的认识。

二、使用仓储的意义

1、站在领域层更过关心领域逻辑的层面,上面说了,仓储作为领域层和基础结构层的连接组件,使得领域层不必过多的关注存储细节。在设计时,将仓储接口放在领域层,而将仓储的具体实现放在基础结构层,领域层通过接口访问数据存储,而不必过多的关注仓储存储数据的细节(也就是说领域层不必关心你用EntityFrameWork还是NHibernate来存储数据),这样使得领域层将更多的关注点放在领域逻辑上面。

2、站在架构的层面,仓储解耦了领域层和ORM之间的联系,这一点也就是很多人设计仓储模式的原因,比如我们要更换ORM框架,我们只需要改变仓储的实现即可,对于领域层和仓储的接口基本不需要做任何改变。

三、代码示例

1、解决方案结构图

上面说了,仓储的设计是接口和实现分离的,于是,我们的仓储接口和工作单元接口全部放在领域层,在基础结构层新建了一个仓储的实现类库ESTM.Repository,这个类库需要添加领域层的引用,实现领域层的仓储接口和工作单元接口。所以,通过上图可以看到领域层的IRepositories里面的仓储接口和基础结构层ESTM.Repository项目下的Repositories里面的仓储实现是一一对应的。下面我们来看看具体的代码设计。其实园子里已有很多经典的仓储设计,为了更好地说明仓储的作用,博主还是来班门弄斧下了~~

2、仓储接口

   /// <summary>
/// 仓储接口,定义公共的泛型GRUD
/// </summary>
/// <typeparam name="TEntity">泛型聚合根,因为在DDD里面仓储只能对聚合根做操作</typeparam>
public interface IRepository<TEntity> where TEntity : AggregateRoot
{
#region 属性
IQueryable<TEntity> Entities { get; }
#endregion #region 公共方法
int Insert(TEntity entity); int Insert(IEnumerable<TEntity> entities); int Delete(object id); int Delete(TEntity entity); int Delete(IEnumerable<TEntity> entities); int Update(TEntity entity); TEntity GetByKey(object key);
#endregion
}
    /// <summary>
/// 部门聚合根的仓储接口
/// </summary>
public interface IDepartmentRepository:IRepository<TB_DEPARTMENT>
{ }
    /// <summary>
/// 菜单这个聚合根的仓储接口
/// </summary>
public interface IMenuRepository:IRepository<TB_MENU>
{
IEnumerable<TB_MENU> GetMenusByRole(TB_ROLE oRole);
}
    /// <summary>
/// 角色这个聚合根的仓储接口
/// </summary>
public interface IRoleRepository:IRepository<TB_ROLE>
{
}
    /// <summary>
/// 用户这个聚合根的仓储接口
/// </summary>
public interface IUserRepository:IRepository<TB_USERS>
{
IEnumerable<TB_USERS> GetUsersByRole(TB_ROLE oRole);
}

除了IRepository这个泛型接口,其他4个仓储接口都是针对聚合建立的接口, 上章 C#进阶系列——DDD领域驱动设计初探(一):聚合 介绍了聚合的划分,这里的仓储接口就是基于此建立。IUserRepository接口实现了IRepository接口,并把对应的聚合根传入泛型,这里正好应征了上章聚合根的设计。

3、仓储实现类

  //仓储的泛型实现类
public class EFBaseRepository<TEntity> : IRepository<TEntity> where TEntity : AggregateRoot
{
[Import(typeof(IEFUnitOfWork))]
private IEFUnitOfWork UnitOfWork { get; set; } public EFBaseRepository()
{
       //注册MEF
Regisgter.regisgter().ComposeParts(this);
} public IQueryable<TEntity> Entities
{
get { return UnitOfWork.context.Set<TEntity>(); }
} public int Insert(TEntity entity)
{
UnitOfWork.RegisterNew(entity);
return UnitOfWork.Commit();
} public int Insert(IEnumerable<TEntity> entities)
{
foreach (var obj in entities)
{
UnitOfWork.RegisterNew(obj);
}
return UnitOfWork.Commit();
} public int Delete(object id)
{
var obj = UnitOfWork.context.Set<TEntity>().Find(id);
if (obj == null)
{
return ;
}
UnitOfWork.RegisterDeleted(obj);
return UnitOfWork.Commit();
} public int Delete(TEntity entity)
{
UnitOfWork.RegisterDeleted(entity);
return UnitOfWork.Commit();
} public int Delete(IEnumerable<TEntity> entities)
{
foreach (var entity in entities)
{
UnitOfWork.RegisterDeleted(entity);
}
return UnitOfWork.Commit();
} public int Update(TEntity entity)
{
UnitOfWork.RegisterModified(entity);
return UnitOfWork.Commit();
} public TEntity GetByKey(object key)
{
return UnitOfWork.context.Set<TEntity>().Find(key);
}
}

仓储的泛型实现类里面通过MEF导入工作单元,工作单元里面拥有连接数据库的上下文对象。

  [Export(typeof(IDepartmentRepository))]
public class DepartmentRepository : EFBaseRepository<TB_DEPARTMENT>,IDepartmentRepository
{
}
    [Export(typeof(IMenuRepository))]
public class MenuRepository:EFBaseRepository<TB_MENU>,IMenuRepository
{
public IEnumerable<TB_MENU> GetMenusByRole(TB_ROLE oRole)
{
throw new Exception();
}
}
    [Export(typeof(IRoleRepository))]
public class RoleRepository:EFBaseRepository<TB_ROLE>,IRoleRepository
{ }
    [Export(typeof(IUserRepository))]
public class UserRepository:EFBaseRepository<TB_USERS>,IUserRepository
{
public IEnumerable<TB_USERS> GetUsersByRole(TB_ROLE oRole)
{
throw new NotImplementedException();
}
}

仓储是4个具体实现类里面也可以通过基类里面导入的工作单元对象UnitOfWork去操作数据库。

4、工作单元接口

   //工作单元基类接口
public interface IUnitOfWork
{
bool IsCommitted { get; set; } int Commit(); void Rollback();
}
    //仓储上下文工作单元接口,使用这个的一般情况是多个仓储之间存在事务性的操作,用于标记聚合根的增删改状态
public interface IUnitOfWorkRepositoryContext:IUnitOfWork,IDisposable
{
/// <summary>
/// 将聚合根的状态标记为新建,但EF上下文此时并未提交
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="obj"></param>
void RegisterNew<TEntity>(TEntity obj)
where TEntity : AggregateRoot; /// <summary>
/// 将聚合根的状态标记为修改,但EF上下文此时并未提交
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="obj"></param>
void RegisterModified<TEntity>(TEntity obj)
where TEntity : AggregateRoot; /// <summary>
/// 将聚合根的状态标记为删除,但EF上下文此时并未提交
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="obj"></param>
void RegisterDeleted<TEntity>(TEntity obj)
where TEntity : AggregateRoot;
}

看到这两个接口可能有人就有疑惑了,为什么要设计两个接口,直接合并一个不行么?这个工作单元的设计思路来源dax.net的系列文章,再次表示感谢!的确,刚开始,博主也有这种疑惑,仔细思考才知道,应该是出于事件机制来设计的,实现IUnitOfWorkRepositoryContext这个接口的都是针对仓储设计的工作单元,而实现IUnitOfWork这个接口除了仓储的设计,可能还有其他情况,比如事件机制。

5、工作单元实现类

    //表示EF的工作单元接口,因为DbContext是EF的对象
public interface IEFUnitOfWork : IUnitOfWorkRepositoryContext
{
DbContext context { get; }
}

为什么要在这里还设计一层接口?因为博主觉得,工作单元要引入EF的Context对象,同理,如果你用的NH,那么这里应该是引入Session对象

/// <summary>
/// 工作单实现类
/// </summary>
[Export(typeof(IEFUnitOfWork))]
public class EFUnitOfWork : IEFUnitOfWork
{
#region 属性
//通过工作单元向外暴露的EF上下文对象
public DbContext context { get { return EFContext; } } [Import(typeof(DbContext))]
public DbContext EFContext { get; set; }
#endregion #region 构造函数
public EFUnitOfWork()
{
//注册MEF
Regisgter.regisgter().ComposeParts(this);
}
#endregion #region IUnitOfWorkRepositoryContext接口
public void RegisterNew<TEntity>(TEntity obj) where TEntity : AggregateRoot
{
var state = context.Entry(obj).State;
if (state == EntityState.Detached)
{
context.Entry(obj).State = EntityState.Added;
}
IsCommitted = false;
} public void RegisterModified<TEntity>(TEntity obj) where TEntity : AggregateRoot
{
if (context.Entry(obj).State == EntityState.Detached)
{
context.Set<TEntity>().Attach(obj);
}
context.Entry(obj).State = EntityState.Modified;
IsCommitted = false;
} public void RegisterDeleted<TEntity>(TEntity obj) where TEntity : AggregateRoot
{
context.Entry(obj).State = EntityState.Deleted;
IsCommitted = false;
}
#endregion #region IUnitOfWork接口 public bool IsCommitted { get; set; } public int Commit()
{
if (IsCommitted)
{
return ;
}
try
{
int result = context.SaveChanges();
IsCommitted = true;
return result;
}
catch (DbUpdateException e)
{ throw e;
}
} public void Rollback()
{
IsCommitted = false;
}
#endregion #region IDisposable接口
public void Dispose()
{
if (!IsCommitted)
{
Commit();
}
context.Dispose();
}
#endregion
}

工作单元EFUnitOfWork上面注册了MEF的Export,是为了供仓储的实现基类里面Import,同理,这里有一点需要注意的,这里要想导入DbContext,那么EF的上下文对象就要Export

    [Export(typeof(DbContext))]
public partial class ESTMContainer:DbContext
{
}

这里用了万能的部分类partial,还记得上章说到的领域Model么,也是在edmx的基础上通过部分类在定义的。同样,在edmx的下面肯定有一个EF自动生成的上下文对象,如下:

  public partial class ESTMContainer : DbContext
{
public ESTMContainer()
: base("name=ESTMContainer")
{
} protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
} public DbSet<TB_DEPARTMENT> TB_DEPARTMENT { get; set; }
public DbSet<TB_MENU> TB_MENU { get; set; }
public DbSet<TB_MENUROLE> TB_MENUROLE { get; set; }
public DbSet<TB_ROLE> TB_ROLE { get; set; }
public DbSet<TB_USERROLE> TB_USERROLE { get; set; }
public DbSet<TB_USERS> TB_USERS { get; set; }
}

上文中多个地方用到了注册MEF的方法

Regisgter.regisgter().ComposeParts(this);

是因为我们在基础结构层里面定义了注册方法

namespace ESTM.Infrastructure.MEF
{
public class Regisgter
{
private static object obj =new object();
private static CompositionContainer _container;
public static CompositionContainer regisgter()
{
lock (obj)
{
try
{
if (_container != null)
{
return _container;
}
AggregateCatalog aggregateCatalog = new AggregateCatalog();
string path = AppDomain.CurrentDomain.BaseDirectory;
var thisAssembly = new DirectoryCatalog(path, "*.dll");
if (thisAssembly.Count() == )
{
path = path + "bin\\";
thisAssembly = new DirectoryCatalog(path, "*.dll");
}
aggregateCatalog.Catalogs.Add(thisAssembly);
_container = new CompositionContainer(aggregateCatalog);
return _container;
}
catch (Exception ex)
{
return null;
}
}
}
}
}

6、Demo测试

为了测试我们搭的框架能运行通过,我们在应用层里面写一个测试方法。正常情况下,应用层ESTM.WCF.Service项目只需要添加ESTM.Domain项目的引用,那么在应用层里面如何找到仓储的实现呢?还是我们万能的MEF,通过IOC依赖注入的方式,应用层不必添加仓储实现层的引用,通过MEF将仓储实现注入到应用层里面,但前提是应用层的bin目录下面要有仓储实现层生成的dll,需要设置ESTM.Repository项目的生成目录为ESTM.WCF.Service项目的bin目录。这个问题在C#进阶系列——MEF实现设计上的“松耦合”(终结篇:面向接口编程)这篇里面介绍过

还是来看看测试代码

namespace ESTM.WCF.Service
{
class Program
{ [Import]
public IUserRepository userRepository { get; set; } static void Main(string[] args)
{
var oProgram = new Program();
Regisgter.regisgter().ComposeParts(oProgram); var lstUsers = oProgram.userRepository.Entities.ToList();
}
}
}

运行得到结果:

7、总结

至此,我们框架仓储的大致设计就完了,我们回过头来看看这样设计的优势所在:

(1)仓储接口层和实现层分离,使得领域模型更加纯净,领域模型只关注仓储的接口,而不用关注数据存储的具体细节,使得领域模型将更多的精力放在领域业务上面。

(2)应用层只需要引用领域层,只需要调用领域层里面的仓储接口就能得到想要的数据,而不用添加仓储具体实现的引用,这也正好符合项目解耦的设计。

(3)更换ORM方便。项目现在用的是EF,若日后需要更换成NH,只需要再实现一套仓储和上下文即可。这里需要说明一点,由于整个框架使用EF的model First,为了直接使用EF的model,我们把edmx定义在了领域层里面,其实这样是不合理的,但是我们为了使用简单,直接用了partial定义领域模型的行为,如果要更好的使用DDD的设计,EF现在的Code First是最好的方式,领域层里面只定义领域模型和关注领域逻辑,EF的CRUD放在基础结构层,切换ORM就真的只需要重新实现一套仓储即可,这样的设计才是博主真正想要的效果,奈何时间和经历有限,敬请谅解。以后如果有时间博主会分享一个完整设计的DDD。

C#进阶系列——DDD领域驱动设计初探(二):仓储Repository(上)的更多相关文章

  1. C#进阶系列——DDD领域驱动设计初探(一):聚合

    前言:又有差不多半个月没写点什么了,感觉这样很对不起自己似的.今天看到一篇博文里面写道:越是忙人越有时间写博客.呵呵,似乎有点道理,博主为了证明自己也是忙人,这不就来学习下DDD这么一个听上去高大上的 ...

  2. C#进阶系列——DDD领域驱动设计初探(三):仓储Repository(下)

    前言:上篇介绍了下仓储的代码架构示例以及简单分析了仓储了使用优势.本章还是继续来完善下仓储的设计.上章说了,仓储的最主要作用的分离领域层和具体的技术架构,使得领域层更加专注领域逻辑.那么涉及到具体的实 ...

  3. C#进阶系列——DDD领域驱动设计初探(四):WCF搭建

    前言:前面三篇分享了下DDD里面的两个主要特性:聚合和仓储.领域层的搭建基本完成,当然还涉及到领域事件和领域服务的部分,后面再项目搭建的过程中慢慢引入,博主的思路是先将整个架构走通,然后一步一步来添加 ...

  4. C#进阶系列——DDD领域驱动设计初探(五):AutoMapper使用

    前言:前篇搭建了下WCF的代码,就提到了DTO的概念,对于为什么要有这么一个DTO的对象,上章可能对于这点不太详尽,在此不厌其烦再来提提它的作用: 从安全上面考虑,领域Model都带有领域业务,让Cl ...

  5. C#进阶系列——DDD领域驱动设计初探(六):领域服务

    前言:之前一直在搭建项目架构的代码,有点偏离我们的主题(DDD)了,这篇我们继续来聊聊DDD里面另一个比较重要的知识点:领域服务.关于领域服务的使用,书中也介绍得比较晦涩,在此就根据博主自己的理解谈谈 ...

  6. C#进阶系列——DDD领域驱动设计初探(七):Web层的搭建

    前言:好久没更新博客了,每天被该死的业务缠身,今天正好一个模块完成了,继续来完善我们的代码.之前的六篇完成了领域层.应用层.以及基础结构层的部分代码,这篇打算搭建下UI层的代码. DDD领域驱动设计初 ...

  7. DDD领域驱动设计初探(七):Web层的搭建

    前言:好久没更新博客了,每天被该死的业务缠身,今天正好一个模块完成了,继续来完善我们的代码.之前的六篇完成了领域层.应用层.以及基础结构层的部分代码,这篇打算搭建下UI层的代码. DDD领域驱动设计初 ...

  8. DDD领域驱动设计初探(六):领域服务

    前言:之前一直在搭建项目架构的代码,有点偏离我们的主题(DDD)了,这篇我们继续来聊聊DDD里面另一个比较重要的知识点:领域服务.关于领域服务的使用,书中也介绍得比较晦涩,在此就根据博主自己的理解谈谈 ...

  9. DDD领域驱动设计初探(五):AutoMapper使用

    前言:前篇搭建了下WCF的代码,就提到了DTO的概念,对于为什么要有这么一个DTO的对象,上章可能对于这点不太详尽,在此不厌其烦再来提提它的作用: 从安全上面考虑,领域Model都带有领域业务,让Cl ...

随机推荐

  1. 怎么使用jQuery

    jQuery的强大我何文启(个人主页:hovertree.com)就不用多说了,那么怎么使用jQuery呢? 首先,下载jquery.下载地址:http://hovertree.com/hvtart/ ...

  2. IBM实习

    来到北京,进入IBM实习已经好多天了,两个月的暑假,两个月夏日在这里度过了,并将在未来个一个月里面,仍将在这里走过,但是我却一无所成,现在仍然只在徘徊中游走,丹迪什么时候能真正懂得实习的难得可贵,懂得 ...

  3. SQL Queries from Transactional Plugin Pipeline

    Sometimes the LINQ, Query Expressions or Fetch just doesn't give you the ability to quickly query yo ...

  4. Git常用命令总结

    Git常用命令总结 git init      在本地新建一个repo,进入一个项目目录,执行git init,会初始化一个repo,并在当前文件夹下创建一个.git文件夹.   git clone ...

  5. android 双缓存机制

    废话不多说,直接贴代码! 所谓的双缓存,第一就是缓存在内存里面,第二就是缓存在SD卡里面,当你需要加载数据时,先去内存缓存中查找,如果没有再去SD卡中查找,并且用户可以自选使用哪种缓存! 缓存内存和缓 ...

  6. android handler ,message消息发送方式

    1.Message msg =  Message.obtain(mainHandler) msg.obj=obj;//添加你需要附加上去的内容 msg.what = what;//what消息处理的类 ...

  7. 使用jsonp跨域调用百度js实现搜索框智能提示,并实现鼠标和键盘对弹出框里候选词的操作【附源码】

    项目中常常用到搜索,特别是导航类的网站.自己做关键字搜索不太现实,直接调用百度的是最好的选择.使用jquery.ajax的jsonp方法可以异域调用到百度的js并拿到返回值,当然$.getScript ...

  8. css3【语法要点】

    语法要点 display: -webkit-box; /* 老版本语法: Safari, iOS, Android browser, older WebKit browsers. */ display ...

  9. 《java JDK7 学习笔记》之接口与多态

    1.对于"定义行为"也就是接口,可以使用interface关键字定义,接口中的方法不能操作,直接标示为abstract,而且一定是public修饰的. 类要操作接口,必须使用imp ...

  10. redis 集群热备自动切换sentinel配置实战

    ---恢复内容开始--- Redis SentinelSentinel(哨兵)是用于监控redis集群中Master状态的工具,其已经被集成在redis2.4+的版本中一.Sentinel作用:1): ...