|
新年伊始,祝大家喜樂如意,愛和幸?!笆蟆辈槐M!?. ??.?? 概述在上一篇 《如何運(yùn)用領(lǐng)域驅(qū)動設(shè)計(jì) - 存儲庫》 的文章中,我們講述了有關(guān)倉儲的概念和使用規(guī)范。倉儲為聚合提供了持久化到本地的功能,但是在持久化的過程中,有時(shí)一個(gè)聚合根中的各個(gè)領(lǐng)域?qū)ο髸稚⒌讲煌臄?shù)據(jù)庫表里面;又或者是一個(gè)用例操作需要操作多個(gè)倉儲;而這些操作都應(yīng)該要么同時(shí)成功,要么同時(shí)失敗,因此就需要為這一系列操作提供事務(wù)的支持,而事務(wù)管理就是由工作單元來提供的。在上一篇中,可能已經(jīng)提到了工作單元,但是僅僅是一筆帶過,現(xiàn)在我們就來詳細(xì)的探究該如何更好的來實(shí)現(xiàn)工作單元。(文章的代碼片段都使用的是C#,案例項(xiàng)目也是基于 DotNet Core 平臺)。 在這里我們可以先來看一下,該項(xiàng)目的應(yīng)用代碼是什么樣子: [HttpPost]
public ActionResult<string> Add()
{
//使用倉儲來處理聚合
_itineraryRepository.Add(
new Itinerary(
"奧特曼",
"賽文奧特曼",
"杰克奧特曼",
"佐菲奧特曼",
"泰羅奧特曼"));
_itineraryRepository.Add(
new Itinerary(
"蓋亞奧特曼",
"戴拿奧特曼",
"阿古茹奧特曼",
"迪迦奧特曼", ""));
return "success";
}
[HttpGet]
public ActionResult<long> Get()
{
var count = _itineraryRepository.GetCount();
return count;
}這是在Aspnet Core的Controller中的代碼,也就是對外提供的Api。可以看到我們僅僅只是通過倉儲的調(diào)用就完成了所有的操作。(ps:原諒我該演示api沒有遵循restful風(fēng)格( ̄▽ ̄)",還有就是那些奧特曼。。。)。 您可能會說,這里沒有做操作,那肯定是在 ItineraryRepository 里面做了手腳。好吧,下面我們來看看該倉儲的實(shí)現(xiàn)。 public class ItineraryRepository
: EFRepository<UowAppDbContext, Itinerary, Guid>
{
public void Add(Itinerary itinerary) => DbContext.Set<Itinerary>().Add(itinerary);
}是的,它也只有這么一點(diǎn)點(diǎn)代碼。而作為后期的業(yè)務(wù)擴(kuò)展和維護(hù),我們只需要完善我們的Itinerary聚合(為它擴(kuò)展行為和增加實(shí)體或值對象)以及ItineraryRepository倉儲(為它添加對外檢索意圖的方法)就可以了。 這種做法的好處可能您很快就能發(fā)現(xiàn):在我們代碼中處處都是關(guān)于領(lǐng)域?qū)ο蟮牟僮?,盡可能的避免其它基礎(chǔ)構(gòu)建或功能支持組件來干擾程序。除了代碼量的減少之外,它也讓可讀性有著明顯的提高,如果在此基礎(chǔ)上能夠構(gòu)建出明確而干凈的聚合根,那么您的程序?qū)⒕邆涓叩目蓴U(kuò)展性。 好吧,回到我們今天的主題:工作單元。其實(shí)上面的代碼就是對倉儲中工作單元的巧妙運(yùn)用,它其實(shí)在后面默默的支持著程序的正常運(yùn)轉(zhuǎn),這是在調(diào)用層面上我們完全感覺不到它的存在而已。下面就為您介紹它是怎么工作和實(shí)現(xiàn)的。 什么是工作單元按照國際管理呢,這一章節(jié)都是解讀有關(guān)原著《領(lǐng)域驅(qū)動設(shè)計(jì):軟件核心復(fù)雜性應(yīng)對之道》 中的解釋。但是?。?!有關(guān)工作單元的概念在書里并沒有被明確的提及到。所以為了證明我們確確實(shí)實(shí)是在前人的基礎(chǔ)理念上來實(shí)踐,而不是胡編亂造自己隨便弄了一個(gè)概念出來。我特地去找了另外一本較為權(quán)威的領(lǐng)域驅(qū)動設(shè)計(jì)教材:《領(lǐng)域驅(qū)動設(shè)計(jì)模式、原理與實(shí)踐》 。在該書中對工作單元的解釋如下:
其實(shí)上文的話真的很好理解(相對于原著而言( ̄y▽, ̄)╭ )。首先我們可以得到的第一個(gè)結(jié)論:事務(wù)管理其實(shí)是應(yīng)用服務(wù)層干的事。第二個(gè)結(jié)論:事務(wù)的協(xié)調(diào)管理都是由工作單元來負(fù)責(zé)的 所以,我們千萬不能因?yàn)楣ぷ鲉卧蛡}儲有聯(lián)系就將它放置在領(lǐng)域?qū)永锩?/strong>:事務(wù)的提供往往是由數(shù)據(jù)庫管理程序來提供的,而這一類組件我們一般將它們放置在基礎(chǔ)構(gòu)架層,而領(lǐng)域?qū)涌梢砸蕾囉诨A(chǔ)構(gòu)架層,所以千萬要注意,保持您的領(lǐng)域?qū)幼銐蚋蓛?,不要讓其它的東西干擾它,也更不要將事務(wù)處理這類東西放到了您的領(lǐng)域?qū)觼?。(這一點(diǎn),您會在后期MiCake<米蛋糕>的使用中看到詳細(xì)的案例)。 如何實(shí)現(xiàn)工作單元實(shí)現(xiàn)工作單元,就是要實(shí)現(xiàn)倉儲中的事務(wù)操作。您可能已經(jīng)看到過有些實(shí)現(xiàn)Repository的框架,它的寫法是注入一個(gè)unitOfWork,然后從uow中提取一個(gè)倉儲,然后再用倉儲來完成聚合根的持久化操作。類似的代碼就像這樣: var yourRepository = uow.GetRepository<yourRepository>(); yourRepository.Add(yourEntity); uow.Commit(); 這樣做沒有一點(diǎn)點(diǎn)的問題,而且是對工作單元和倉儲模式的完美實(shí)現(xiàn)。uow工作單元中維持了一個(gè)事務(wù),從該工作單元中創(chuàng)建的每一個(gè)倉儲都可以獲得該事務(wù),倉儲完成了自己的操作之后,工作單元使用Commit方法告訴事務(wù)管理器,該事務(wù)完成。 夏目去參加了妖怪的聚會,一回到家,貓咪老師就發(fā)現(xiàn)了它沾染了妖怪的味道
懶的模式其實(shí)在剛開始,為 MiCake(米蛋糕) 選取工作單元實(shí)現(xiàn)方案的時(shí)候,我也打算采用這種方式。但是在思考了一天之后,我還是放棄了。因?yàn)槲野l(fā)現(xiàn)這種模式在完成每一次倉儲操作的時(shí)候,必須要從工作單元中去獲取。在Aspnet Core中,不得不在Controller中注入工作單元對象,然后再從該對象里面去獲取倉儲。這顯然削弱了依賴注入所為我們提供的依賴閱讀性(原本在構(gòu)造函數(shù)中,我能看出我需要注入的是A倉儲,但是現(xiàn)在我看到的只有工作單元)。 其實(shí)最重要的一點(diǎn)就是:我太懶啦 o_o ....。 為什么每次都要去多寫一個(gè)uow.GetXXXXX()。每使用一個(gè)倉儲就要多寫一次獲取語句,我就不能好好的只使用倉儲嗎? 所以在這個(gè)想法的強(qiáng)烈刺激下,我選取了另外的實(shí)現(xiàn)方法。 接下來,就讓我們來實(shí)現(xiàn)最開始演示代碼中的工作單元吧。哦,對了,忘記說了,無論是演示的Github Demo還是本次的博文,我們都選取了Entity Framework Core來作為數(shù)據(jù)持久組件。所以有些小伙伴會說,那我使用Dapper或者原生的ADO怎么辦? 其實(shí)思路都是一樣的,您也可以在看了EFCore的版本后,自己寫出對應(yīng)的工作單元版本。如果有機(jī)會的話,歡迎在Github的Demo上直接添加,就可以提交供更多的同學(xué)參考啦。 實(shí)現(xiàn)思路
腦袋里有了這些還比較模糊的交互對象之后,我們可以來想一下一個(gè)倉儲完成添加聚合根的操作是怎么樣的:
雖然步驟好像有5步,但總結(jié)下來,就是將具有事務(wù)的對象放置到工作單元中,讓它去負(fù)責(zé)提交。對!就是這么簡單,該方法與上面那種從工作單元中獲取倉儲的方法想法,它是往工作單元中提交。所以,我們此時(shí)可以構(gòu)造出一個(gè)偽代碼出來,大致理解它的實(shí)現(xiàn): //1、使用工作單元管理器創(chuàng)建一個(gè)工作單元
using (var uow = unitOfWorkManager.Create())
{
//2、構(gòu)造事務(wù)特征對象,開啟事務(wù)并注冊到工作單元
RegisteTransactonFeature(DbContext);
//3、執(zhí)行倉儲中的內(nèi)容
DbContext.Set<Itinerary>().Add(itinerary)
//4、工作單元保存提交
uow.SaveChanges();
//5、dispose
}至少到目前,我們可以抽象出上面的各個(gè)對象了。 //首先是事務(wù)特征對象,它提供了事務(wù)的基本Commit和Rollback方法
public interface ITransactionFeature
{
public bool IsCommit { get; }
public bool IsRollback { get; }
void Commit();
Task CommitAsync(CancellationToken cancellationToken = default);
void Rollback();
Task RollbackAsync(CancellationToken cancellationToken = default);
}
//然后是事務(wù)特征容器,它具有增加刪除事務(wù)特征對象的方法
public interface ITransactionFeatureContainer
{
void RegisteTranasctionFeature(string key, ITransactionFeature TransactionFeature);
ITransactionFeature GetOrAddTransactionFeature(string key, ITransactionFeature TransactionFeature);
ITransactionFeature GetTransactionFeature(string key);
void RemoveTransaction(string key);
}
//接下來是工作單元,它實(shí)現(xiàn)了事務(wù)特征容器,并且對外提供提交的方法
public interface IUnitOfWork : ITransactionFeatureContainer
{
Guid ID { get; }
bool IsDisposed { get; }
void SaveChanges();
Task SaveChangesAsync(CancellationToken cancellationToken = default);
void Rollback();
Task RollbackAsync(CancellationToken cancellationToken = default);
}
//最后是工作單元管理器,它提供了創(chuàng)建工作單元的方法
public interface IUnitOfWorkManager : IUnitOfWokrProvider, IDisposable
{
IUnitOfWork Create();
}落地代碼在構(gòu)建出接口之后,我們就可以寫出具體的實(shí)現(xiàn)類了。首先是實(shí)現(xiàn)工作單元(UnitOfWork)對象。(由于具體代碼實(shí)現(xiàn)較多,講解部分只選取了核心部分,完整代碼可以參考Github的項(xiàng)目) public class UnitOfWork : IUnitOfWork
{
private readonly Dictionary<string, ITransactionFeature> _transactionFeatures;
public UnitOfWork()
{
_transactionFeatures = new Dictionary<string, ITransactionFeature>();
}
//往容器中添加事物特征對象
public virtual ITransactionFeature GetOrAddTransactionFeature(
[NotNull]string key,
[NotNull] ITransactionFeature transcationFeature)
{
if (_transactionFeatures.ContainsKey(key))
return _transactionFeatures.GetValueOrDefault(key);
_transactionFeatures.Add(key, transcationFeature);
return transcationFeature;
}
//對外提供的保存方法,執(zhí)行該方法時(shí)調(diào)用容器內(nèi)所有事物特征對象的Commit方法
public virtual void SaveChanges()
{
foreach (var transactionFeature in _transactionFeatures.Values)
{
transactionFeature.Commit();
}
}
}接下來就是與ORM框架關(guān)聯(lián)最深的事務(wù)特征對象的實(shí)現(xiàn)了,由于我們選取了EF,所以此處應(yīng)該實(shí)現(xiàn)EF版本的事務(wù)特征對象: public class EFTransactionFeature : ITransactionFeature
{
private IDbContextTransaction _dbContextTransaction;
private DbContext _dbContext;
public EFTransactionFeature(DbContext dbContext)
{
_dbContext = dbContext;
}
//設(shè)置事務(wù)
public void SetTransaction(IDbContextTransaction dbContextTransaction)
{
_isOpenTransaction = true;
_dbContextTransaction = dbContextTransaction;
}
public void Commit()
{
if (IsCommit)
return;
IsCommit = true;
//EF 事務(wù)的提交
_dbContext.SaveChanges();
_dbContextTransaction?.Commit();
}
}建立好了這兩個(gè)對象之后,其實(shí)我們只需要一個(gè)流轉(zhuǎn)過程就可以實(shí)現(xiàn)工作單元了。這個(gè)流程就是將事務(wù)特征對象添加到工作單元中,但是我們應(yīng)該在什么時(shí)候?qū)⑺砑舆M(jìn)去呢?看過第一版Github代碼的小伙伴可能知道,在倉儲調(diào)用的時(shí)候就可以完成該操作。當(dāng)時(shí)在第一版中,我們的實(shí)現(xiàn)代碼是這樣的: public class EFRepository
{
protected IUnitOfWorkManager UnitOfWorkManager { get; private set; }
protected DbContext DbContext { get; private set; }
public EFRepository(IUnitOfWorkManager unitOfWorkManager, DbContext dbContext)
{
UnitOfWorkManager = unitOfWorkManager;
DbContext = dbContext;
}
public void Add(TAggregateRoot aggregateRoot)
{
RegistUnitOfWork(DbContext);
DbContext.Set<TAggregateRoot>().Add(aggregateRoot);
}
private void RegistUnitOfWork(DbContext dbContext)
{
string key = $"EFTransactionFeature - {dbContext.ContextId.InstanceId.ToString()}";
unitOfWork.ResigtedTransactionFeature(key, new EFTransactionFeature(DbContext));
}
}在每一次進(jìn)行倉儲操作的時(shí)候,都調(diào)用了一個(gè)RegistUnitOfWork的方法,來完成事務(wù)特征對象和工作單元的流轉(zhuǎn)工作。但是很快您就能發(fā)現(xiàn)問題:EFRepository是我們實(shí)現(xiàn)的一個(gè)基類,以后所有的倉儲操作都繼承該類來完成操作,那不是每擴(kuò)展一個(gè)方法,我都要在該方法中寫一句注冊代碼?如果我忘記寫了怎么辦。還有一點(diǎn),該注冊過程并沒有開啟一個(gè)事務(wù),那么事務(wù)是怎么來的呢? 那么怎么才能避免用戶每一次都要去顯示調(diào)用注冊呢,而是讓用戶在不知不覺中就完成了該操作。所以我們得思考在每一個(gè)方法中,用戶都一定會寫的代碼是什么,然后在該代碼上下手??赡苣呀?jīng)想到了,DbContext?。?!是的,每一個(gè)方法里,用戶都會去寫DbContext,所以我們可以在他獲取DbContext的時(shí)候就完成注冊操作。所以,優(yōu)化后的代碼就是這樣的: public class EFRepository
{
public virtual TDbContext DbContext
{
get => _dbContextFactory.CreateDbContext();
}
public void Add(TAggregateRoot aggregateRoot)
{
DbContext.Set<TAggregateRoot>().Add(aggregateRoot);
}
}而該_dbContextFactory的實(shí)現(xiàn)就更簡單了,他要完成的任務(wù)就是注冊到工作單元并且開啟事務(wù)。 internal class UowDbContextFactory<TDbContext>
{
private readonly IUnitOfWorkManager _uowManager;
public UowDbContextFactory(IUnitOfWorkManager uowManager)
{
_uowManager = uowManager;
}
public TDbContext CreateDbContext()
{
AddDbTransactionFeatureToUow(currentUow, DbContext);
return wantedDbContext;
}
private void AddDbTransactionFeatureToUow(IUnitOfWork uow, TDbContext dbContext)
{
string key = $"EFCore - {dbContext.ContextId.InstanceId.ToString()}";
var efFeature = uow.GetOrAddTransactionFeature(key, new EFTransactionFeature(dbContext));
if (IsFeatureNeedOpenTransaction(uow, efFeature))
{
var dbcontextTransaction = dbContext.Database.BeginTransaction();
efFeature.SetTransaction(dbcontextTransaction);
}
}
private bool IsFeatureNeedOpenTransaction(IUnitOfWork uow, EFTransactionFeature efFeature)
{
return !efFeature.IsOpenTransaction;
}
}
此時(shí),我們就已經(jīng)實(shí)現(xiàn)了工作單元的流轉(zhuǎn)了,那么還有一個(gè)問題就是:我們怎么默認(rèn)去實(shí)現(xiàn)一個(gè)工作單元,而不是每一次都需要手動去開啟并提交。 AspNet Core為我們提供了很好的攔截方法。第一種方法: 我們可以在中間件中完成,因?yàn)樗械恼埱蠖家┻^中間件,我們可以在方法到API之前就開啟事務(wù),等API訪問結(jié)束后就提交事務(wù)。第二種方法: 通過IActionFilter等周期接口來完成。本案例選取了第一種實(shí)現(xiàn)方法,您也可以根據(jù)您自己的愛好選取自己的實(shí)現(xiàn)方式。 缺陷到這里我們已經(jīng)實(shí)現(xiàn)了像上面Demo版本的工作單元,但是該工作單元其實(shí)還有許多特性沒有實(shí)現(xiàn):
不過如果您的項(xiàng)目僅僅使用了一種ORM框架并且只需要開啟一個(gè)工作單元,那么可以嘗試使用該實(shí)現(xiàn)。 在實(shí)現(xiàn)MiCake真正的工作單元中,我嘗試了很多方法來解決上面的問題。在后面的文章中,您也會看到MiCake真正的工作單元。 附上一個(gè)當(dāng)時(shí)寫工作單元的手記( ̄︶ ̄)↗ 總結(jié)本來這篇文章不打算寫在《如何運(yùn)用領(lǐng)域驅(qū)動設(shè)計(jì)》這個(gè)系列的,但是后來糾結(jié)了一下,還是納入了該系列。由于該篇文章是實(shí)現(xiàn)工作單元的,所以代碼量就比較大,希望不會給您造成閱讀上的困難。下一篇的文章,是一個(gè)談了很久的問題————持久化值對象,現(xiàn)在終于是時(shí)候該解決它了。在本次Demo中您看到的聚合根Itinerary所有的屬性都是string,很顯然這是不符合常理的,所以在下一次就要讓它成為真正的領(lǐng)域?qū)ο?。?em>ps:改成真正的領(lǐng)域?qū)ο蠛螅杏X都可以單體DDD應(yīng)用落地了呢。( ̄︶ ̄)↗醒醒!少年。)為了您不錯(cuò)過下一篇文章的內(nèi)容,您也可也點(diǎn)擊博客園右上角的關(guān)注,這樣就能及時(shí)收到更新了喲。 |
|
|