記錄一次使用UnifOfWork改造項目的過程。
一、前言
UnifOfWork模式,一般稱為“工作單元”模式,是DDD(Domain-Driven Design,領域驅動設計)的一個組成部分,用於在應用服務方法中,控制一個應用服務方法的所有數據庫操作,都在一個事務內。最近對個人的一個Web項目進行了改造,按照DDD的設計模式進行了分層,雖然在領域層沒有實現完整的領域對象(實體+值對象+領域服務 ),但是DDD也並不是鐵板一塊,架構設計指南最終也是要服務於具體項目的,領悟思想為我所用才是一個提高的過程,記錄下來,以備日後溫習。
二、DDD模式分析
經典的DDD架構,由如圖四層組成,第一層是用戶層,是直接面向用戶的UI界面層,放到開發環境中,可以指代比如瀏覽器前端、移動端APP、Windows客戶端等。第二層是指應用服務層,這層直接面向的是現實的需求,比如某公司的“新員工入職”,這一層根據DDD理論,應該是很薄的一層,主要是組合領域層中的不同對象,來實現最終的現實需求。本文主要實現的UnitOfWork也是在這一層。
第三層領域層,這是整個系統中最核心的一層,表達了業務的邏輯,按照通用語言、和使用通用語言對於業務範圍進行限界上下文的劃分,劃分出一塊一塊的領域出來,並使用“實體對象、值對象、Repository、領域服務”等概念進行組織。其中Repository應該是抽象的接口,為了解耦合具體的ORM,Repository模式應該是在領域層定義接口、在第四層基礎設施層實現(以備日後可能的進行更換ORM行為)。
第四層基礎設施層,是項目中一些具體底層操作的實現,比如領域層的Repository的實現,文件的保存,郵件的發送,等等。
領域驅動設計是一門精深的學問,以上只簡單介紹,如果有興趣,可以去看看這兩本書: 《領域驅動設計 軟件核心復雜性應對之道》《實現領域驅動設計》。
三、項目分層簡介
個人的項目規模也不大,但是會有“在同一次Http請求內同一事務內組合業務邏輯”的需求,結合ASP.NET的已有功能,分層如下:
用戶界面層:Web SPA(Angular、React等,前端分離另行開發,故VS中不會有相關的項目)
應用服務層:ASP.NET WebAPI
領域層:一個單獨的類庫項目,定義Model(貧血模型,只包含屬性)、服務的接口IXXXService以及其實現XXXService,倉儲接口IRepository。
基礎設施層:一個單獨的類庫項目:實現了領域層中定義的IRepository,目前已有了EF以及Dapper的兩種實現。
項目引用(依賴)順序:最頂層是領域層,無任何依賴;應用訪問層(WebAPI)依賴領域層以及基礎設施層。
在領域層中,我定義如下兩種基礎接口,其中IUnitOfWork接口包含一個虛擬事務對象、以及一個Commit()方法。IRepository對象用於實現對於每一個實體(model或者叫entity都可以)的持久化操作,處於簡單期間並沒有加入比如分頁查詢方法等。
public interface IUnitOfWork { /// <summary> /// 事務對象,Dapper為IDbTransaction,EF為DbContext /// </summary> object VirtualTransaction { get; } void Commit(); } public interface IRepository<T> where T:class { int Insert(T t); T Get(string id); int Update(T t); int Delete(T t); }
因為ASP.NET Core自帶了一個方便的IOC容器,可以實現Transient(每次使用都創建新的對象)、Scope(每個Http請求內只創建唯一的對象)、Singletion(單例)三種生命周期的依賴註入,結合ASP.NET的工作原理:對於每一個Http請求,都會實例化一個Controller對其處理,因此我們可以進行如下設計:
在ASP.NET Core的StartUp中,將IUnitOfWork與其實現類UnitOfwork,進行Scope方式註入。
這樣,每次Http請求中,因為自始至終只會有一個UnitOfWork對象,這個對象裏面保存著一個用於事務的對象(EF是DbContext,Dapper是IDBTransaction),在Action結束的時候,調用UnitOfWork的Commit()方法,進行提交,即實現了每個Controller中只會有一個事務。
//UnitOfWork會被註入到Repo對象中,Repo對象會被註入到Service對象中,Service對象會被註入到Controller對象中
services.AddScoped<IUnitOfWork,UnitOfWork>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IBookRepository, BookRepository>();
services.AddScoped<IAccountService, AccountService>();
services.AddScoped<IPublishService, PublishService>();
再貼一下我的Controller的示意代碼
[Route("api/[controller]")] public class BusinessController : Controller { private readonly IAccountService _accountService; private readonly IPublishService _publishService; private readonly IUnitOfWork _unitOfWork; public BusinessController( IPublishService publishService, IAccountService accountService, IUnitOfWork unitOfWork) { _accountService = accountService; _publishService = publishService; _unitOfWork = unitOfWork; } //這裏寫具體的WebAPI的Action方法,方法中可以調用各種Service中的業務邏輯方法,然後在方法的末尾,加上_unitOfWork.Commit()即可 }
WebAPI作為整個項目的應用服務層、自身沒有任何業務邏輯,而是組合業務邏輯層中的各種Service,完成具體的現實的用戶使用需求。
比如一個現實需求是“一個作家,註冊成為了作協會員,並登記了他的出版作品”。這個需求可以拆解為兩個業務邏輯:1.註冊 2.登記出版物 。我們在領域層中實現了這兩個業務邏輯後,就可以在應用訪問層(我們的Controller中)進行組合、並在同一個事務過程中控制他們了。有人可能會問:你這麽做,有什麽必要呢,我直接三層架構那樣,Controller調用業務邏輯層BLL中的一個“註冊並登記出版物”方法,然後這個BLL方法直接去調用DAL中各種數據庫操作方法,不也可以實現你所說的嗎?
好,這時候,應用服務層+UnitOfWork模式的優點就可以體現出來了。如果是按照傳統三層那樣,一個BLL方法“註冊並登記出版物”,他就只能用於“註冊並登記出版物”了,無法實現業務邏輯的組合復用。而使用DDD提倡的編程模式,就可以實現多種業務邏輯組合復用,比如我前面距離的“註冊”“登記出版物”這兩個領域服務方法,同時還可以組合其他業務邏輯實現更多不同的現實需求中,比如“註冊”+“繳費”、“登記出版物”+“領取津貼”,這裏每一個需求都是在同一個事務內的,也可以保證數據的一致性。
貼一個IService方法和Service,都在領域層中
public interface IPublishService { void PublishNewBook(Book book); } public class PublishService : IPublishService { private readonly IBookRepository _bookRepository; public PublishService(IBookRepository bookRepository) { _bookRepository = bookRepository; } public void PublishNewBook(Book book) { _bookRepository.Insert(book); } }
貼一個IRepository(領域層中)和它的對應實現(Repository)
//領域層中的Book倉儲接口,這裏簡單起見,沒有使用更多的接口方法 public interface IBookRepository:IRepository<Book> { } //基礎設施層中使用Dapper操作數據庫的實現 public abstract class BaseRepository { private readonly IDbTransaction _dbTransaction; protected internal IDbConnection DbConnection { get; } protected BaseRepository(IUnitOfWork unitOfWork) { _dbTransaction = (unitOfWork.VirtualTransaction) as IDbTransaction; DbConnection = _dbTransaction.Connection; } public CommandDefinition GenCmd(string cmdText, object paramObj) { CommandDefinition cmd = new CommandDefinition(cmdText, paramObj, _dbTransaction); return cmd; } } public class BookRepository :BaseRepository, IBookRepository { public BookRepository(IUnitOfWork unitOfWork):base(unitOfWork) { } //這裏只寫了Insert方法,其他的可類比 public int Insert(Book t) { string cmdText = "INSERT INTO Book (Id,BookName,Author,Price,PublishTime) VALUES (@Id,@BookName,@Author,@Price,@PublishTime)"; var cmd = GenCmd(cmdText, t);
var result = DbConnection.Execute(cmd); return result; } } //另外一個基礎設施層項目中,使用EF操作數據庫的實現 public abstract class BaseRepository { protected internal DbContext DbContext { get; } protected BaseRepository(IUnitOfWork unitOfWork) { DbContext = unitOfWork.VirtualTransaction as DbContext; } } public class BookRepository : BaseRepository ,IBookRepository { public BookRepository(IUnitOfWork unitOfWork):base(unitOfWork) { } //這裏只寫了Inert方法,其他的可以類比實現 public int Insert(Book t) { DbContext.Book.Add(t);
//這裏不能寫DbContext.SaveChanges(),因為EF的SaveChanges()是一個對所有更改的事務性提交,而根據我們的設計事務不在此處提交,而是在應用服務層的Controller中 return 1; } }
然後,我們就可以在Controller中組合不同Service提供的業務邏輯了,並且可以在同一個事務內,有效的提高了業務邏輯層的復用程度同時保證了同一個Http請求內數據的事務一致性。
記錄一次使用UnifOfWork改造項目的過程。