标签 标签 标签

  • 一句话的事儿:

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

  • C#进阶系列——DDD领域驱动设计初探(一):聚合
  • C#进阶系列——DDD领域驱动设计初探(二):仓储Repository(上)
  • C#进阶系列——DDD领域驱动设计初探(三):仓储Repository(下)
  • C#进阶系列——DDD领域驱动设计初探(四):WCF搭建
  • C#进阶系列——DDD领域驱动设计初探(五):AutoMapper使用
  • C#进阶系列——DDD领域驱动设计初探(六):领域服务
  • C#进阶系列——DDD领域驱动设计初探(七):Web层的搭建

    一、仓储的定义

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

    二、使用仓储的意义

    1、站在领域层更过关心领域逻辑的层面,上面说了,仓储作为领域层和基础结构层的连接组件,使得领域层不必过多的关注存储细节。在设计时,将仓储接口放在领域层,而将仓储的具体实现放在基础结构层,领域层通过接口访问数据存储,而不必过多的关注仓储存储数据的细节(也就是说领域层不必关心你用EntityFrameWork还是NHibernate来存储数据),这样使得领域层将更多的关注点放在领域逻辑上面。
    2、站在架构的层面,仓储解耦了领域层和ORM之间的联系,这一点也就是很多人设计仓储模式的原因,比如我们要更换ORM框架,我们只需要改变仓储的实现即可,对于领域层和仓储的接口基本不需要做任何改变。

    三、代码示例

    1、解决方案结构图

    DDD领域驱动设计初探(二):仓储Repository(上) - 图1

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

    2、仓储接口

    ```csharp /// /// 仓储接口,定义公共的泛型GRUD /// /// 泛型聚合根,因为在DDD里面仓储只能对聚合根做操作 public interface IRepository where TEntity : AggregateRoot {
    1. #region 属性
    2. IQueryable<TEntity> Entities { get; }
    3. #endregion
    4. #region 公共方法
    5. int Insert(TEntity entity);
    6. int Insert(IEnumerable<TEntity> entities);
    7. int Delete(object id);
    8. int Delete(TEntity entity);
    9. int Delete(IEnumerable<TEntity> entities);
    10. int Update(TEntity entity);
    11. TEntity GetByKey(object key);
    12. #endregion
    }

///

/// 部门聚合根的仓储接口 /// public interface IDepartmentRepository:IRepository { }

///

/// 菜单这个聚合根的仓储接口 /// public interface IMenuRepository:IRepository { IEnumerable GetMenusByRole(TB_ROLE oRole); }

///

/// 角色这个聚合根的仓储接口 /// public interface IRoleRepository:IRepository { }

///

/// 用户这个聚合根的仓储接口 /// public interface IUserRepository:IRepository { IEnumerable GetUsersByRole(TB_ROLE oRole); }

  1. 除了IRepository这个泛型接口,其他4个仓储接口都是针对聚合建立的接口, 上章 [C#进阶系列——DDD领域驱动设计初探(一):聚合](http://www.cnblogs.com/landeanfen/p/4816706.html) 介绍了聚合的划分,这里的仓储接口就是基于此建立。IUserRepository接口实现了IRepository接口,并把对应的聚合根传入泛型,这里正好应征了上章聚合根的设计。
  2. <a name="lkWE9"></a>
  3. ### 3、仓储实现类
  4. ```csharp
  5. //仓储的泛型实现类
  6. public class EFBaseRepository<TEntity> : IRepository<TEntity> where TEntity : AggregateRoot
  7. {
  8. [Import(typeof(IEFUnitOfWork))]
  9. private IEFUnitOfWork UnitOfWork { get; set; }
  10. public EFBaseRepository()
  11. {
  12. //注册MEF
  13. Regisgter.regisgter().ComposeParts(this);
  14. }
  15. public IQueryable<TEntity> Entities
  16. {
  17. get { return UnitOfWork.context.Set<TEntity>(); }
  18. }
  19. public int Insert(TEntity entity)
  20. {
  21. UnitOfWork.RegisterNew(entity);
  22. return UnitOfWork.Commit();
  23. }
  24. public int Insert(IEnumerable<TEntity> entities)
  25. {
  26. foreach (var obj in entities)
  27. {
  28. UnitOfWork.RegisterNew(obj);
  29. }
  30. return UnitOfWork.Commit();
  31. }
  32. public int Delete(object id)
  33. {
  34. var obj = UnitOfWork.context.Set<TEntity>().Find(id);
  35. if (obj == null)
  36. {
  37. return 0;
  38. }
  39. UnitOfWork.RegisterDeleted(obj);
  40. return UnitOfWork.Commit();
  41. }
  42. public int Delete(TEntity entity)
  43. {
  44. UnitOfWork.RegisterDeleted(entity);
  45. return UnitOfWork.Commit();
  46. }
  47. public int Delete(IEnumerable<TEntity> entities)
  48. {
  49. foreach (var entity in entities)
  50. {
  51. UnitOfWork.RegisterDeleted(entity);
  52. }
  53. return UnitOfWork.Commit();
  54. }
  55. public int Update(TEntity entity)
  56. {
  57. UnitOfWork.RegisterModified(entity);
  58. return UnitOfWork.Commit();
  59. }
  60. public TEntity GetByKey(object key)
  61. {
  62. return UnitOfWork.context.Set<TEntity>().Find(key);
  63. }
  64. }

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

  1. [Export(typeof(IDepartmentRepository))]
  2. public class DepartmentRepository : EFBaseRepository<TB_DEPARTMENT>,IDepartmentRepository
  3. {
  4. }
  5. [Export(typeof(IMenuRepository))]
  6. public class MenuRepository:EFBaseRepository<TB_MENU>,IMenuRepository
  7. {
  8. public IEnumerable<TB_MENU> GetMenusByRole(TB_ROLE oRole)
  9. {
  10. throw new Exception();
  11. }
  12. }
  13. [Export(typeof(IRoleRepository))]
  14. public class RoleRepository:EFBaseRepository<TB_ROLE>,IRoleRepository
  15. {
  16. }
  17. [Export(typeof(IUserRepository))]
  18. public class UserRepository:EFBaseRepository<TB_USERS>,IUserRepository
  19. {
  20. public IEnumerable<TB_USERS> GetUsersByRole(TB_ROLE oRole)
  21. {
  22. throw new NotImplementedException();
  23. }
  24. }

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

4、工作单元接口

  1. //工作单元基类接口
  2. public interface IUnitOfWork
  3. {
  4. bool IsCommitted { get; set; }
  5. int Commit();
  6. void Rollback();
  7. }
  8. //仓储上下文工作单元接口,使用这个的一般情况是多个仓储之间存在事务性的操作,用于标记聚合根的增删改状态
  9. public interface IUnitOfWorkRepositoryContext:IUnitOfWork,IDisposable
  10. {
  11. /// <summary>
  12. /// 将聚合根的状态标记为新建,但EF上下文此时并未提交
  13. /// </summary>
  14. /// <typeparam name="TEntity"></typeparam>
  15. /// <param name="obj"></param>
  16. void RegisterNew<TEntity>(TEntity obj)
  17. where TEntity : AggregateRoot;
  18. /// <summary>
  19. /// 将聚合根的状态标记为修改,但EF上下文此时并未提交
  20. /// </summary>
  21. /// <typeparam name="TEntity"></typeparam>
  22. /// <param name="obj"></param>
  23. void RegisterModified<TEntity>(TEntity obj)
  24. where TEntity : AggregateRoot;
  25. /// <summary>
  26. /// 将聚合根的状态标记为删除,但EF上下文此时并未提交
  27. /// </summary>
  28. /// <typeparam name="TEntity"></typeparam>
  29. /// <param name="obj"></param>
  30. void RegisterDeleted<TEntity>(TEntity obj)
  31. where TEntity : AggregateRoot;
  32. }

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

5、工作单元实现类

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

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

  1. /// <summary>
  2. /// 工作单实现类
  3. /// </summary>
  4. [Export(typeof(IEFUnitOfWork))]
  5. public class EFUnitOfWork : IEFUnitOfWork
  6. {
  7. #region 属性
  8. //通过工作单元向外暴露的EF上下文对象
  9. public DbContext context { get { return EFContext; } }
  10. [Import(typeof(DbContext))]
  11. public DbContext EFContext { get; set; }
  12. #endregion
  13. #region 构造函数
  14. public EFUnitOfWork()
  15. {
  16. //注册MEF
  17. Regisgter.regisgter().ComposeParts(this);
  18. }
  19. #endregion
  20. #region IUnitOfWorkRepositoryContext接口
  21. public void RegisterNew<TEntity>(TEntity obj) where TEntity : AggregateRoot
  22. {
  23. var state = context.Entry(obj).State;
  24. if (state == EntityState.Detached)
  25. {
  26. context.Entry(obj).State = EntityState.Added;
  27. }
  28. IsCommitted = false;
  29. }
  30. public void RegisterModified<TEntity>(TEntity obj) where TEntity : AggregateRoot
  31. {
  32. if (context.Entry(obj).State == EntityState.Detached)
  33. {
  34. context.Set<TEntity>().Attach(obj);
  35. }
  36. context.Entry(obj).State = EntityState.Modified;
  37. IsCommitted = false;
  38. }
  39. public void RegisterDeleted<TEntity>(TEntity obj) where TEntity : AggregateRoot
  40. {
  41. context.Entry(obj).State = EntityState.Deleted;
  42. IsCommitted = false;
  43. }
  44. #endregion
  45. #region IUnitOfWork接口
  46. public bool IsCommitted { get; set; }
  47. public int Commit()
  48. {
  49. if (IsCommitted)
  50. {
  51. return 0;
  52. }
  53. try
  54. {
  55. int result = context.SaveChanges();
  56. IsCommitted = true;
  57. return result;
  58. }
  59. catch (DbUpdateException e)
  60. {
  61. throw e;
  62. }
  63. }
  64. public void Rollback()
  65. {
  66. IsCommitted = false;
  67. }
  68. #endregion
  69. #region IDisposable接口
  70. public void Dispose()
  71. {
  72. if (!IsCommitted)
  73. {
  74. Commit();
  75. }
  76. context.Dispose();
  77. }
  78. #endregion
  79. }

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

  1. [Export(typeof(DbContext))]
  2. public partial class ESTMContainer:DbContext
  3. {
  4. }

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

  1. public partial class ESTMContainer : DbContext
  2. {
  3. public ESTMContainer()
  4. : base("name=ESTMContainer")
  5. {
  6. }
  7. protected override void OnModelCreating(DbModelBuilder modelBuilder)
  8. {
  9. throw new UnintentionalCodeFirstException();
  10. }
  11. public DbSet<TB_DEPARTMENT> TB_DEPARTMENT { get; set; }
  12. public DbSet<TB_MENU> TB_MENU { get; set; }
  13. public DbSet<TB_MENUROLE> TB_MENUROLE { get; set; }
  14. public DbSet<TB_ROLE> TB_ROLE { get; set; }
  15. public DbSet<TB_USERROLE> TB_USERROLE { get; set; }
  16. public DbSet<TB_USERS> TB_USERS { get; set; }
  17. }

上文中多个地方用到了注册MEF的方法
Regisgter.regisgter().ComposeParts(this);
是因为我们在基础结构层里面定义了注册方法
DDD领域驱动设计初探(二):仓储Repository(上) - 图2

  1. namespace ESTM.Infrastructure.MEF
  2. {
  3. public class Regisgter
  4. {
  5. private static object obj =new object();
  6. private static CompositionContainer _container;
  7. public static CompositionContainer regisgter()
  8. {
  9. lock (obj)
  10. {
  11. try
  12. {
  13. if (_container != null)
  14. {
  15. return _container;
  16. }
  17. AggregateCatalog aggregateCatalog = new AggregateCatalog();
  18. string path = AppDomain.CurrentDomain.BaseDirectory;
  19. var thisAssembly = new DirectoryCatalog(path, "*.dll");
  20. if (thisAssembly.Count() == 0)
  21. {
  22. path = path + "bin\\";
  23. thisAssembly = new DirectoryCatalog(path, "*.dll");
  24. }
  25. aggregateCatalog.Catalogs.Add(thisAssembly);
  26. _container = new CompositionContainer(aggregateCatalog);
  27. return _container;
  28. }
  29. catch (Exception ex)
  30. {
  31. return null;
  32. }
  33. }
  34. }
  35. }
  36. }

6、Demo测试

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

  1. namespace ESTM.WCF.Service
  2. {
  3. class Program
  4. {
  5. [Import]
  6. public IUserRepository userRepository { get; set; }
  7. static void Main(string[] args)
  8. {
  9. var oProgram = new Program();
  10. Regisgter.regisgter().ComposeParts(oProgram);
  11. var lstUsers = oProgram.userRepository.Entities.ToList();
  12. }
  13. }
  14. }

运行得到结果:
DDD领域驱动设计初探(二):仓储Repository(上) - 图3

7、总结

至此,我们框架仓储的大致设计就完了,我们回过头来看看这样设计的优势所在:
(1)仓储接口层和实现层分离,使得领域模型更加纯净,领域模型只关注仓储的接口,而不用关注数据存储的具体细节,使得领域模型将更多的精力放在领域业务上面。
(2)应用层只需要引用领域层,只需要调用领域层里面的仓储接口就能得到想要的数据,而不用添加仓储具体实现的引用,这也正好符合项目解耦的设计。
(3)更换ORM方便。项目现在用的是EF,若日后需要更换成NH,只需要再实现一套仓储和上下文即可。这里需要说明一点,由于整个框架使用EF的model First,为了直接使用EF的model,我们把edmx定义在了领域层里面,其实这样是不合理的,但是我们为了使用简单,直接用了partial定义领域模型的行为,如果要更好的使用DDD的设计,EF现在的Code First是最好的方式,领域层里面只定义领域模型和关注领域逻辑,EF的CRUD放在基础结构层,切换ORM就真的只需要重新实现一套仓储即可,这样的设计才是博主真正想要的效果,奈何时间和经历有限,敬请谅解。以后如果有时间博主会分享一个完整设计的DDD。


  • 本文作者:GeekPower - Felix Sun
  • 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!