文章出處

上一篇:《DDD 領域驅動設計-談談 Repository、IUnitOfWork 和 IDbContext 的實踐(1)

閱讀目錄:

  1. 抽離 IRepository 并改造 Repository

  2. IUnitOfWork 和 Application Service 的變化

  3. 總結三種設計方案

簡單總結上篇所做的兩個改進:

  • 從 Repository 和 UnitOfWork 中抽離出 IDbContext,并且它們只依賴于 IDbContext。
  • Repository 和 UnitOfWork 為平級關系,UnitOfWork 負責維護對象狀態(增刪改),Repository 負責獲取對象(查)。

后來,園友 Qlin 在評論中,提出了另外一種方式,大致為:

  • Repository 和 UnitOfWork 還是依賴于 IDbContext。
  • UnitOfWork 只有 Commit,Repository 提供對象的所有操作(增刪改查)。

這篇文章我們就按照這種方式實現一下,關于 Repository、IUnitOfWork 和 IDbContext 的設計,以及 Application Service 的調用,上面是兩種設計方案,加上上一篇博文開頭說到的一種方案,我大致總結了三種,關于它們的優缺點,文章最后我再進行總結。

另外,關于 IDbContext 的接口設計,其實是有些模糊的,因為它并沒有真正解耦 EF,比如 DbSet<TEntity> Set<TEntity>() 還是依賴于 EF,沒辦法,就像我們在 Repository 中返回 IQueryable,你在 Application Service 調用的時候,也必須引用 EF 一樣,對于 IDbContext 來說,我們暫時把它看作是一個數據上下文容器,所有對象的持久化最后都通過它來完成,因為我們的解決方案暫時只能使用 EF,所以對于 IDbContext,我們先暫時這樣設計。

下面我們開始進行設計。

1. 抽離 IRepository 并改造 Repository

抽離 IRepository 啥意思?我們直接來看下代碼:

namespace DDD.Sample.Domain.IRepository
{
    public interface IRepository<TAggregateRoot> 
        where TAggregateRoot : class, IAggregateRoot
    {
        void Add(TAggregateRoot aggregateRoot);

        void Update(TAggregateRoot aggregateRoot);

        void Delete(TAggregateRoot aggregateRoot);

        TAggregateRoot Get(int id);
    }
}

IRepository 是一個泛型接口,類型為 IAggregateRoot,我們在里面定義了增刪改查的常用操作,它的作用就是減少 Repository 的冗余代碼,我們看下 IStudentRepository 的定義:

namespace DDD.Sample.Domain.IRepository
{
    public interface IStudentRepository : IRepository<Student>
    {
        Student GetByName(string name);
    }
}

IStudentRepository 需要繼承 IRepository,并確定泛型類型為 Student,Student 繼承自 IAggregateRoot,因為增刪改查常用操作已經定義,所以我們在其它類似的 IStudentRepository 中就不需要定義了。

IRepository 需要進行實現,如果在 StudentRepository 中進行實現,就沒有什么作用了,所以我們需要一個 BaseRepository 來實現 IRepository:

namespace DDD.Sample.Repository
{
    public abstract class BaseRepository<TAggregateRoot> : IRepository<TAggregateRoot>
        where TAggregateRoot : class, IAggregateRoot
    {
        public readonly IDbContext _dbContext;

        public BaseRepository(IDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public void Add(TAggregateRoot aggregateRoot)
        {
            _dbContext.Set<TAggregateRoot>().Add(aggregateRoot);
        }

        public void Update(TAggregateRoot aggregateRoot)
        {
            _dbContext.Entry<TAggregateRoot>(aggregateRoot).State = EntityState.Modified;
        }

        public void Delete(TAggregateRoot aggregateRoot)
        {
            _dbContext.Set<TAggregateRoot>().Remove(aggregateRoot);
        }

        public TAggregateRoot Get(int id)
        {
            return _dbContext.Set<TAggregateRoot>().FirstOrDefault(t => t.Id == id);
        }
    }
}

咋一看 BaseRepository 有點像我們上篇的 UnitOfWork,因為我們把增刪改放在 Repository 了,因為 Repository 還是和 UnitOfWork 為平級關系,所以我們在 Repository 中用的 IDbContext 而非 IUnitOfWork,這個沒什么問題,我們看下 StudentRepository 的具體實現:

namespace DDD.Sample.Repository
{
    public class StudentRepository : BaseRepository<Student>, IStudentRepository
    {
        public StudentRepository(IDbContext dbContext)
            : base(dbContext)
        {
        }

        public Student GetByName(string name)
        {
            return base._dbContext.Set<Student>().Where(x => x.Name == name).FirstOrDefault();
        }
    }
}

StudentRepository 很簡單,因為常用操作 BaseRepository 已經實現了,base(dbContext) 的作用就是給 BaseRepository 注入 IDbContext 對象。

Repository 的改造基本上就這些,表面看起來確實很好,另外,如果沒有 IUnitOfWork 和 Application Service,我們對 Domain 進行單元測試,也是能滿足我們的需求,但需要將 IDbContext 再進行修改下。

2. IUnitOfWork 和 Application Service 的變化

我們先看下 IUnitOfWork 的變化,直接貼下代碼:

namespace DDD.Sample.Infrastructure.Interfaces
{
    public interface IUnitOfWork
    {
        bool Commit();

        void Rollback();
    }
}

因為增刪改都移到 Repository 中了,所以 IUnitOfWork 的工作就很簡單,只有 Commit 和 Rollback,實現也比較簡單,我們看下:

namespace DDD.Sample.Infrastructure
{
    public class UnitOfWork : IUnitOfWork
    {
        private IDbContext _dbContext;

        public UnitOfWork(IDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public bool Commit()
        {
            return _dbContext.SaveChanges() > 0;
        }

        public void Rollback()
        {
            throw new NotImplementedException();
        }
    }
}

這個沒啥說的,我們直接看下 Application Service 的代碼:

namespace DDD.Sample.Application
{
    public class StudentService : IStudentService
    {
        private IUnitOfWork _unitOfWork;
        private IStudentRepository _studentRepository;
        private ITeacherRepository _teacherRepository;

        public StudentService(IUnitOfWork unitOfWork, 
            IStudentRepository studentRepository,
            ITeacherRepository teacherRepository)
        {
            _unitOfWork = unitOfWork;
            _studentRepository = studentRepository;
            _teacherRepository = teacherRepository;
        }

        public Student Get(int id)
        {
            return _studentRepository.Get(id);
        }

        public bool Add(string name)
        {
            var student = new Student { Name = name };
            var teacher = _teacherRepository.Get(1);
            teacher.StudentCount++;

            _studentRepository.Add(student);
            _teacherRepository.Update(teacher);
            return _unitOfWork.Commit();
        }
    }
}

StudentService 其實變化不大,只是將原來的 _unitOfWork 添加修改操作,改成了 _studentRepository 和 _teacherRepository,執行下 StudentService.Add 的單元測試代碼,發現執行不通過,為什么呢?因為 Repository 和 UnitOfWork 的 IDbContext 不是同一個對象,添加修改對象通過 Repository 注冊到 IDbContext 中,最后 UnitOfWork 執行 Commit 卻是另一個 IDbContext,所以我們需要確保 Repository 和 UnitOfWork 共享一個 IDbContext 對象,怎么實現呢?

我們進行改造下:

namespace DDD.Sample.Application
{
    public class StudentService : IStudentService
    {
        private IDbContext _dbContext;
        private IUnitOfWork _unitOfWork;
        private IStudentRepository _studentRepository;
        private ITeacherRepository _teacherRepository;

        public StudentService(IDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public Student Get(int id)
        {
            _studentRepository = new StudentRepository(_dbContext);
            return _studentRepository.Get(id);
        }

        public bool Add(string name)
        {
            _unitOfWork = new UnitOfWork(_dbContext);
            _studentRepository = new StudentRepository(_dbContext);
            _teacherRepository = new TeacherRepository(_dbContext);

            var student = new Student { Name = name };
            var teacher = _teacherRepository.Get(1);
            teacher.StudentCount++;

            _studentRepository.Add(student);
            _teacherRepository.Update(teacher);
            return _unitOfWork.Commit();
        }
    }
}

上面對應的測試代碼執行通過,其實解決方式很簡單,就是手動給 UnitOfWork、StudentRepository 和 TeacherRepository 注入相同的 IDbContext 對象,當然這是一種解決方式,還有人喜歡用屬性注入,這都是可以的,無非最后就是想讓 Repository 和 UnitOfWork 共享一個 IDbContext 對象。

本篇的相關代碼已提交到 GitHub,大家可以參考下:https://github.com/yuezhongxin/DDD.Sample

3. 總結三種設計方案

關于 Repository、IUnitOfWork 和 IDbContext 的設計,以及 Application Service 的調用,我總結了三種設計方式,我覺得也是我們常用的幾種方式,下面我大致分別說下。

1. IUnitOfWork -> EfUnitOfWork -> Repository -> Application Service

這種設計應該我們最熟悉,因為我們一開始就是這樣設計的,但問題也是最多的,要不然我也不會寫上一篇博文了,比如存在的問題:

  • IUnitOfWork 的職責不明確。
  • Repository 的職責不明確。
  • Application Service 很困惑,因為它不知道該使用誰。
  • Application Service 的代碼越來越亂。
  • ....

上一篇博文最后分析出來是 IUnitOfWork 的設計問題,因為它做了太多的事,并且 Repository 依賴于 IUnitOfWork,以至于最后在 Application Service 的調用中,Repository 顯得非常多余,這種設計最大的問題就是職責不明確

2. IDbContext -> IUnitOfWork/IRepository(only query) -> UnitOfWork/Repository -> Application Service

第二種設計是我比較傾向于的,因為第一種設計出現的問題,所以我對 IUnitOfWork 的設計非常看重,并且我讀了《企業應用架構模式》中關于 UnitOfWork 的所有內容,其實就那么幾個字可以概括:維護對象狀態,統一提交更改。我個人覺得架構設計最重要的地方就是底層接口的設計,就像我們蓋一棟摩天大樓,如果地基打不穩,最后的結果肯定是垮塌,所以,我比較堅持 IUnitOfWork 這樣的設計:

相對于第一種設計,這種設計還有一個不同就是 IUnitOfWork 和 IRepository 為平級關系,為什么這樣設計?因為我們不能通過 IUnitOfWork 提供查詢操作,并且 IUnitOfWork 和 ORM 也沒什么關系,所以我們最后抽離出來一個 IDbContext,并且用 EF 去實現它。

IRepository 只有查詢,這是我們的定義,在 Application Service 的調用中,對象的新增和修改都是通過 IUnitOfWork 進行實現的,因為查詢并不需要記錄狀態,所以我們并不需要將 IDbContext 在 IUnitOfWork 和 IRepository 之間進行共享,有人會說,IRepository 應該提供領域對象的增刪改操作啊,我們再看下 Repository 的定義:協調領域和數據映射層,利用類似于集合的接口來訪問領域對象。

集合訪問領域對象,那 Repository 如果這樣設計呢:

public class StudentRepository : IStudentRepository
{
    private IQueryable<Student> _students;

    public StudentRepository(IDbContext dbContext)
    {
        _students = dbContext.Set<Student>();
    }

    public Student GetByName(string name)
    {
        return _students.Where(x => x.Name == name).FirstOrDefault();
    }
}

這種 Repository 設計是比較符合定義的,另外,我們如果對 Domain 進行單元測試,集合性質的領域對象也是可以進行維護的,只不過沒有持久化而已。

總的來說,第二種設計最大的優點就是職責明確,你想干壞事也干不了(因為接口已經被約束),目前來說沒發現什么問題。

3. IDbContext -> IUnitOfWork(only commit)/IRepository -> UnitOfWork/Repository -> Application Service

第三種設計就是本篇博文講述的,它其實是從第一種和第二種之間取一個中間值,做了一些妥協工作,具體的實現,上面已經詳細說明了,我最接受不了的是對 IUnitOfWork 的更改,雖然表面看起來蠻好的,但我總覺得有些不對勁的地方,就像我們“迫于現實做一些違背道德的事”,可能現在覺察不到什么,但出來混的總是要還的。

關于 Repository、IUnitOfWork 和 IDbContext 的設計,以及 Application Service 的調用,我覺得應該是我們在 DDD 架構設計過程中,最普遍遇到的一個問題,但也是最困惑的一個問題,比如最近兩個園友寫的博文:

對于本篇博文,如果你有什么問題或疑問,歡迎探討學習。:)


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()