DDD領域驅動設計初探(二):倉儲Repository(上)

前言:上篇介紹了DDD設計Demo裏面的聚合劃分以及實體和聚合根的設計,這章繼續來講說DDD裏面最具爭議的話題之一的倉儲Repository,爲何Repository會有這麼大的爭議,博主認爲主要緣由無非如下兩點:一是Repository的真實意圖沒有理解清楚,致使設計的紊亂,隨着項目的橫向和縱向擴展,到最後愈來愈難維護;二是趕時髦的爲了「模式」而「模式」,倉儲並不是適用於全部項目,這就像沒有任何一種架構能解決全部的設計難題同樣。本篇經過這個設計的Demo來談談博主對倉儲的理解,有不對的地方還望園友們斧正!html

DDD領域驅動設計初探系列文章:數據庫

1、倉儲的定義

倉儲,顧名思義,存儲數據的倉庫。那麼有人就疑惑了,既然咱們有了數據庫來存取數據,爲何還要弄一個倉儲的概念,其實博主以爲這是一個考慮層面不一樣的問題,數據庫主要用於存取數據,而倉儲做用之一是用於數據的持久化。從架構層面來講,倉儲用於鏈接領域層和基礎結構層,領域層經過倉儲訪問存儲機制,而不用過於關心存儲機制的具體細節。按照DDD設計原則,倉儲的做用對象的領域模型的聚合根,也就是說每個聚合都有一個單獨的倉儲。可能這樣說你們未必能理解,相信看了倉儲的代碼設計,你們能有一個更加透徹的認識。編程

2、使用倉儲的意義

一、站在領域層更過關心領域邏輯的層面,上面說了,倉儲做爲領域層和基礎結構層的鏈接組件,使得領域層沒必要過多的關注存儲細節。在設計時,將倉儲接口放在領域層,而將倉儲的具體實現放在基礎結構層,領域層經過接口訪問數據存儲,而沒必要過多的關注倉儲存儲數據的細節(也就是說領域層沒必要關心你用EntityFrameWork仍是NHibernate來存儲數據),這樣使得領域層將更多的關注點放在領域邏輯上面。架構

二、站在架構的層面,倉儲解耦了領域層和ORM之間的聯繫,這一點也就是不少人設計倉儲模式的緣由,好比咱們要更換ORM框架,咱們只須要改變倉儲的實現便可,對於領域層和倉儲的接口基本不須要作任何改變。app

3、代碼示例

一、解決方案結構圖

 

 

上面說了,倉儲的設計是接口和實現分離的,因而,咱們的倉儲接口和工做單元接口所有放在領域層,在基礎結構層新建了一個倉儲的實現類庫ESTM.Repository,這個類庫須要添加領域層的引用,實現領域層的倉儲接口和工做單元接口。因此,經過上圖能夠看到領域層的IRepositories裏面的倉儲接口和基礎結構層ESTM.Repository項目下的Repositories裏面的倉儲實現是一一對應的。下面咱們來看看具體的代碼設計。其實園子裏已有不少經典的倉儲設計,爲了更好地說明倉儲的做用,博主仍是來班門弄斧下了~~框架

二、倉儲接口

複製代碼
   /// <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接口,並把對應的聚合根傳入泛型,這裏正好應徵了上章聚合根的設計。ide

三、倉儲實現類

複製代碼
  //倉儲的泛型實現類
    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 0; } 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去操做數據庫。post

四、工做單元接口

複製代碼
   //工做單元基類接口
    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這個接口除了倉儲的設計,可能還有其餘狀況,好比事件機制。測試

五、工做單元實現類

    //表示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 0;
            }
            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() == 0)
                    {
                        path = path + "bin\\";
                        thisAssembly = new DirectoryCatalog(path, "*.dll");
                    }
                    aggregateCatalog.Catalogs.Add(thisAssembly);
                    _container = new CompositionContainer(aggregateCatalog);
                    return _container;
                }
                catch (Exception ex)
                {
                    return null;
                }
            }
        }
    }
}
複製代碼

六、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();
        }
    }
}
複製代碼

 運行獲得結果:

 

七、總結

至此,咱們框架倉儲的大體設計就完了,咱們回過頭來看看這樣設計的優點所在:

(1)倉儲接口層和實現層分離,使得領域模型更加純淨,領域模型只關注倉儲的接口,而不用關注數據存儲的具體細節,使得領域模型將更多的精力放在領域業務上面。

(2)應用層只須要引用領域層,只須要調用領域層裏面的倉儲接口就能獲得想要的數據,而不用添加倉儲具體實現的引用,這也正好符合項目解耦的設計。

(3)更換ORM方便。項目如今用的是EF,若往後須要更換成NH,只須要再實現一套倉儲和上下文便可。這裏須要說明一點,因爲整個框架使用EF的model First,爲了直接使用EF的model,咱們把edmx定義在了領域層裏面,其實這樣是不合理的,可是咱們爲了使用簡單,直接用了partial定義領域模型的行爲,若是要更好的使用DDD的設計,EF如今的Code First是最好的方式,領域層裏面只定義領域模型和關注領域邏輯,EF的CRUD放在基礎結構層,切換ORM就真的只須要從新實現一套倉儲便可,這樣的設計纔是博主真正想要的效果,奈什麼時候間和經歷有限,敬請諒解。之後若是有時間博主會分享一個完整設計的DDD。

相關文章
相關標籤/搜索