我眼中的領域驅動設計
有幸參與了一些領域驅動的項目,讀了一些文章,也見識了一些不倫不類的架構,感覺對領域驅動有了更進一步的認識。所以今天跟大伙探討一下領域驅動設計,同時也對一些想要實踐領域驅動設計卻又無處下手,或者一些正在實踐卻又說不上領域驅動設計到底好在哪的朋友一些指引方向。當然對于”領域驅動設計”這個主題而言從來不乏爭論,所以大家可以在暢所欲言。
為什么要使用領域驅動設計?
從Eric Evans的《領域驅動設計:軟件核心復雜性應對之道》一書的書名就可以看出這一方法論是為了解決軟件核心復雜性的。也就是說軟件業務越來越復雜了,領域驅動設計可以讓事情變得簡單。而實際情況是:領域驅動設計的門檻很高,沒有很深厚的面向對象編碼能力幾乎不可能實踐成功。
這一說法是否自相矛盾呢?Martin Fowler在PoEAA一書中給了一個有力的解釋:
我們把三層架構等除了領域驅動之外的架構方式都可以歸納為以數據為中心的架構方式,在圖中是黑色的粗實線;
領域驅動設計在圖中是綠色的粗實線。
- 當軟件在開發初期,以數據驅動的架構方式非常容易上手,但是隨著業務的增長和項目的推進,軟件開發和維護難度急劇升高。
- 領域驅動設計則在項目初期就處在一個比較難以上手的位置,但是隨著業務的增長和項目的推進,軟件開發和維護難度平滑上升。
這幅圖形象的解釋了領域驅動設計和傳統的軟件架構模式兩者在軟件開發過程中解決復雜性之間的差異。
領域驅動設計的核心是什么?
顧名思義,領域驅動設計的核心是領域模型,這一方法論可以通俗的理解為先找到業務中的領域模型,以領域模型為中心驅動項目的開發。而領域模型的設計精髓在于面向對象分析,在于對事物的抽象能力,一個領域驅動架構師必然是一個面向對象分析的大師。
在面向對象編程中講究封裝,講究設計低耦合,高內聚的類。而對于一個軟件工程來講,僅僅只靠類的設計是不夠的,我們需要把緊密聯系在一起的業務設計為一個領域模型,讓領域模型內部隱藏一些細節,這樣一來領域模型和領域模型之間的關系就會變得簡單。這一思想有效的降低了復雜的業務之間千絲萬縷的耦合關系。
下圖為“以數據為中心的架構模式”,表和表之間關系錯綜復雜:
下圖是“領域模型”:領域和領域之間只存在大粒度的接口和交互:
初期學習DDD的朋友一定不會錯過Eric Evans寫的《領域驅動設計:軟件核心復雜性應對之道》,這本書名氣很大,也是很多人入門領域驅動設計的首選讀物,這本書提到了領域驅動設計中的一些概念:Repository,Domain,ValueObject等。但是初學者有可能得出一個錯誤的結論:有人誤認為項目架構中加入***Repository,***Domain,***ValueObject就變成了DDD架構。如果沒有悟出其精髓就在項目中加入這些概念,那充其量也不過是個三層架構;反之對于一個面向對象分析的高手而言,不使用這些概念也可以實現領域驅動設計。以Repository的設計為例,我經常看到一些文章中對IRepository定義為:
public interface IRepository<TAggregateRoot>
{
TAggregateRoot Get(int id);
void Remove(TAggregateRoot aggregateRoot);
void Update(TAggregateRoot aggregateRoot);
//What's this?
TAggregateRoot Where(Expression<Func<TAggregateRoot, bool>> filter);
//…
}
領域驅動設計是以領域模型為基本單位,這表明在IRepository<TAggregate>接口中,只有Get(int id),Update(TAggregateRoot aggregate),Remove(TAggregateRoot aggregate)這三個接口是有意義的。Where(Expression<Func<TAggregateRoot,bool>> filter)這一定義暴露了你是在在進行單表操作,對于領域模型來講沒有查詢一說。
而對于IUserRepository這樣一個稍微具體的接口定義:
public interface IUserRepository : IRepository<User>
{
//What's this?
List<Rule> GetRules(int id);
//....
}
一個IUserRepository仍然是一個Repository,他也只能以User聚合根為單位進行操作。方法List<Rule> GetRules(int id)將此Repository打回了原形,這不再是一個Repository,這是一個DAL。
正確的實現方式:
public class User:AggregateRoot
{
private List<Rule> GetRules()
{
return null;
}
public void ApproveRequest(Request request)
{
var rules = user.GetRules();
//......
//如果有權限就批準
}
}
這段代碼體現了User作為一個領域模型,他擁有自己的職責和能力。
如何開始實踐領域驅動設計?
正如本文通篇所說,領域驅動設計講究的是領域模型的分析和對事物的抽象,從來沒有提起過數據如何存取這個話題,言下之意在領域驅動設計中,我們不關心過數據如何存取,怎么樣寫linq效率高,使用懶加載還是include,這些實現細節會將你帶入傳統的三層架構模式中。
在領域驅動設計中要先設計領域模型,接著寫Domain邏輯,至于數據庫,僅僅是用來存儲數據的工具。使用database first那不叫領域驅動設計,很明顯你先設計的表結構,所以應該叫數據庫驅動設計更為準確。更不要引入數據庫獨有的技術,例如觸發器,存儲過程等。數據庫除了存儲數據外,其余一切邏輯都是Domain邏輯。
我們不妨以大家都比較熟悉的醫院門診看病流程舉個例子,看看如何開始實踐領域驅動設計:
我們暫且認為一個門診看病流程就是一個完整的領域模型,此時你要忘掉數據庫,不要再想表結構如何設計,而是就這一領域模型進行抽象:
public class OutPatientProcess:AggregateRoot
{
public Registration _registration { get; private set; }//掛號單
private List<Examination> _examinations;
public IReadOnlyList<Examination> Examinations => _examinations.AsReadOnly();//化驗單
public Prescription Prescription { get; private set; }//處方
public DateTime ConsultaionTime { get; private set; }//接診時間
public Doctor Doctor { get; private set; }//接診醫師
//開始一個門診治療過程
public void StartProcess(Registration registration)
{
_registration = registration;
InquireSymptoms();
WriteOutExamination();
WritePrescription();
}
//詢問病人病情
public void InquireSymptoms()
{
}
//開立化驗單
private void WriteOutExamination()
{
_examinations.Add(new Examination());
}
//填寫處方
private void WritePrescription()
{
}
}
我們暫且不討論這一模型是否符合真實場景,但是這個例子帶你邁入了領域驅動設計的第一步,同時這個例子也向你展示了軟件開發可以不用先設計數據庫。當你寫好所有的Domain邏輯后再考慮把這個類持久化在數據庫中就好了。在我眼中,數據庫僅僅是一個保存數據的東西,不要把他過早的耦合在代碼中。這一強調了很多遍的觀點影響著你能否成功實踐DDD。
CQRS架構展望
話雖這樣說,但是既然你在使用關系數據庫,有人就會免不了跟你提起性能怎么優化這樣的話題。這也是傳統ORM+關系數據庫實現領域驅動設計的硬傷,特別是當你的領域模型Scope設計過大,意味著Repository中的操作每次都要關聯一堆表出來,特別是有人設計數據喜歡遵守第N范式這種基本就沒轍了(沒有貶低遵守這些范式的意思,只是這樣設計的數據庫+ORM會產生較多關聯,相對應的設計為表結構冗余設計,有利于ORM提升性能),不得不說到了最后由于數據庫的存儲性能問題,我們又一次將數據庫納入到了考慮范圍。
解決這一問題的方案是CQRS架構, Query端各種緩存和Nosql,順便把搜索引擎也用上,讓你的軟件飛奔起來。這一架構解耦了數據庫操作,你基本沒有機會跟數據庫打交道并且還解決了數據存儲的性能問題。
這一進化過程也解開了一些人的疑慮,為什么從剛開始寫代碼就開始學習各種設計模式,但是從來沒有機會使用過?因為你所寫的代碼無時無刻不耦合著數據庫這一“毒瘤”,而數據庫操作作為一種實現細節摻雜在你的代碼中,所以領域驅動設計為此而生,你準備好了嗎?
來自:http://www.cnblogs.com/richieyang/archive/2016/04/10/5373250.html