DDD 領域驅動設計-談談 Repository、IUnitOfWork 和 IDbContext 的實踐(2)


原作者: 田園里的蟋蟀 

上一篇:《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 架構設計過程中,最普遍遇到的一個問題,但也是最困惑的一個問題,比如最近兩個園友寫的博文:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM