
已認證的官方帳號
不同于其它的架構方法,領域驅(qū)動設計DDD(Domain Driven Design)提出了從業(yè)務設計到代碼實現(xiàn)一致性的要求,不再對分析模型和實現(xiàn)模型進行區(qū)分。也就是說從代碼的結(jié)構中我們可以直接理解業(yè)務的設計,命名得當?shù)脑挘浅绦蛉藛T也可以“讀”代碼。

然而在整個DDD的建模過程中,我們更多關注的是核心領域模型的建立,我們認為完成業(yè)務的需求就是在領域模型上的一系列操作(應用)。這些操作包括了對核心實體狀態(tài)的改變,領域事件的存儲,領域服務的調(diào)用等。在良好的領域模型之上,實現(xiàn)這些應用應該是輕松而愉快的。
筆者經(jīng)歷過很多次DDD的建模工作坊,在經(jīng)歷了數(shù)天一輪又一輪激烈討論和不厭其煩的審視之后,大家欣慰地看著白板上各種顏色紙貼所展示出來的領域模型,成就感寫滿大家的臉龐。就在這個大功告成的時刻,往往會有人問:這個模型我們怎么落地呢?然后大家臉上的愉悅消失了,換上了對細節(jié)就是魔鬼的焦慮。但這是我們不可避免的實現(xiàn)細節(jié),DDD的原始方法論中雖然給出了“分層架構”(Layered
Architecture)的元模型,但如何分層卻沒有明確定義。
分層架構
在DDD方法提出后的數(shù)年里,分層架構的具體實現(xiàn)也經(jīng)歷了幾代演進,直到Martin
Fowler提煉出下圖的分層實現(xiàn)架構后,才逐步為大家所認可。DDD的方法也得到了有效的補充,模型落地的問題也變得更容易,核心領域模型的范圍也做出了比較明確的定義:包括了Domain,Service
Layer和Repositories。

(Martin
Fowler總結(jié)提出的分層架構實現(xiàn),注意“Resources”是基于RESTful架構的抽象,我們也可以理解為更通用的針對外界的接口Interface。而HTTP
Client主要是針對互聯(lián)網(wǎng)的通信協(xié)議,Gateways實際才是交換過程中組裝信息的邏輯所在。)
我們的核心實體(Entity)和值對象(Value
Object)應該在Domain層,定義的領域服務(Domain Service)在Service
Layer,而針對實體和值對象的存儲和查詢邏輯都應該在Repositories層。值得注意的是,不要把Entity的屬性和行為分離到Domain和Service兩層中去實現(xiàn),即所謂的貧血模型,事實證明這樣的實現(xiàn)方式會造成很大的維護問題。DDD戰(zhàn)術建模中的元模型定義不應該在實現(xiàn)過程中被改變,作為元模型中元素之一的實體本身就應該包含針對自身的行為定義。
基于這個模型,下面我們來談談更具體的代碼結(jié)構。對于這個分層架構還有疑惑的讀者可以精讀一下Martin的原文。有意思的一點是,這個模型的敘述實際是在微服務架構的測試文章中,其中深意值得大家體會。
這里需要明確的是,我們談論代碼結(jié)構的時候,針對的是一個經(jīng)過DDD建模后的子問題域(參見戰(zhàn)略設計篇),這是我們明確的組件化邊界。是否進一步組件化,比如按照限界上下文(Bounded Context)模塊化,或采用微服務架構服務化,核心實體都是進一步可能采用的組件化方法。從抽象層面講,老馬提煉的分層架構適用于面向業(yè)務的服務化架構,所以如果要進一步組件化也是可以按照這個代碼結(jié)構來完成的。
總體的代碼目錄結(jié)構如下:
- DDD-Sample/src/
domain
gateways
interface
repositories
services
這個目錄結(jié)構一一對應了前文的分層架構圖。完整的案例代碼請從GitHub下載。
可以看到實際上我們并沒有建立外部存儲(Data
Mappers/ORM)和對外通信(HTTP
Client)的目錄。從領域模型和應用的角度,這兩者都是我們不必關心的,能夠驗證整個領域模型的輸入和輸出就足夠了。至于什么樣的外部存儲和外部通信機制是可以被“注入”的。這樣的隔離是實現(xiàn)可獨立部署服務的基礎,也是我們能夠測試領域模型實現(xiàn)的要求。

模型表達
根據(jù)分層架構確立了代碼結(jié)構后,我們需要首先定義清楚我們的模型。如前面講到的,這里主要涉及的是從戰(zhàn)術建模過程中得到的核心實體和服務的定義。我們利用C++頭文件(.h文件)來展示一個Domain模型的定義,案例靈感來源于DDD原著里的集裝箱貨運例子。
namespace domain
{
struct Entity
{
int getId();
protected:int id;
};
struct AggregateRoot : Entity{ };
struct ValueObject{ };
struct Provider{ };
struct Delivery : ValueObject
{
Delivery(int);
int AfterDays;
};
struct Cargo : AggregateRoot
{
Cargo(Delivery, int);
~Cargo();
void Delay(int);
private:Delivery delivery;
};
}
這個實現(xiàn)首先申明了元模型實體Entity和值對象ValueObject。實體一定會有一個標識id。在實體的基礎上聲明了DDD中的重要元素聚合根
AggregateRoot。根據(jù)定義,聚合根本身就應該是一個實體,所以AggregateRoot繼承了Entity。
這個案例中我們定義了一個實體Cargo,同時也是一個聚合根。Delivery是一個值對象。雖然這里為了實現(xiàn)效率采用的是struct,在C++里可以理解為定義一個class類。
依賴關系
代碼目錄結(jié)構并不能表達分層體系中各層的依賴關系,比如Domain層是不應該依賴于其它任何一層的。維護各層的依賴關系是至關重要的,很多團隊在實施的過程中都沒有能夠建立起這樣的工程紀律,最后造成代碼結(jié)構的混亂,領域模型也被打破。
根據(jù)分層架構的規(guī)則,我們可以看到示例中的代碼結(jié)構如下圖。

Domain是不依賴于任何的其它對象的。Repositories是依賴于Domain的,實現(xiàn)如下:引用了model.h。
include "model.h"
include
using namespace domain;
namespace repositories {
struct Repository
{
};
...
Services是依賴于Domain和Repositories的,實現(xiàn)如下:引用了model.h和repository.h
include "model.h"
include "repository.h"
using namespace domain;
using namespace repositories;
namespace services {
struct CargoProvider : Provider {
virtual void Confirm(Cargo* cargo){};
};
struct CargoService {
... ...
};
...
為了維護合理的依賴關系,依賴注入(Depedency Injection)是需要經(jīng)常采用的實現(xiàn)模式,它作為解耦合的一種方法相信大家都不會陌生,具體定義參見這里。
在測試構建時,我們利用了一個IoC框架(依賴注入的實現(xiàn))來構造了一個Api,并且把相關的依賴(如CargoService)注入給了這個Api。這樣既沒有破壞Interface和Service的單向依賴關系,又解決了測試過程中Api的實例化要求。
auto provider = std::make_shared< StubCargoProvider >();
api::Api* createApi() {
ContainerBuilder builder;
builder.registerType< CargoRepository >().singleInstance();
builder.registerInstance(provider).as();
builder.registerType< CargoService >().singleInstance();
builder.registerType().singleInstance();
auto container = builder.build();
std::shared_ptr api = container->resolve();
return api.get();
}
測試實現(xiàn)
有了領域模型,大家自然會想著如何去實現(xiàn)業(yè)務應用了,而實現(xiàn)應用的過程中一定會考慮到單元測試的設計。在構建高質(zhì)量軟件過程中,單元測試已經(jīng)成為了標準規(guī)范,但高質(zhì)量的單元測試卻是困擾很多團隊的普遍問題。很多時候設計測試比實現(xiàn)應用本身更加困難。
這里很難有一個固定標準來評判某個時間點的單元測試質(zhì)量,但一個核心的原則是讓用例盡量測試業(yè)務需求而不是實現(xiàn)方式本身。滿足業(yè)務需求是我們的目標,實現(xiàn)方式可能有多種,我們不希望需要持續(xù)重構的實現(xiàn)代碼影響到我們的測試用例。比如針對實現(xiàn)過程中的某個函數(shù)進行入?yún)⒑统鰠⒌膯卧獪y試,當這個函數(shù)發(fā)生一點改變(即使是重命名),我們也需要改動測試。

測試驅(qū)動開發(fā)TDD無疑是一種好的實踐,如果應用得當,它確實能夠?qū)崿F(xiàn)我們上述的原則,并且能夠幫助我們交流業(yè)務的需求。比較有意思的是,在基于DDD建立的核心模型之上應用TDD似乎更加順理成章。類比DDD和TDD雖然是不恰當?shù)?,但我們會發(fā)現(xiàn)兩者在遵循的原則上是一致的,即都是面向業(yè)務做分解和設計:DDD就整個業(yè)務問題域進行了分解,形成子問題域;TDD就業(yè)務需求在實現(xiàn)時進行任務分解,從簡單場景到復雜場景逐步通過測試驅(qū)動出實現(xiàn)。下面的測試用例展現(xiàn)了在核心模型上的TDD過程。
TEST(bc_demo_test, create_cargo)
{
api::CreateCargoMsg* msg = new api::CreateCargoMsg();
msg->Id = ID;
msg->AfterDays = AFTER_DAYS;
createCargo(msg);
EXPECT_EQ(msg->Id, provider->cargo_id);
EXPECT_EQ(msg->AfterDays, provider->after_days);
}
上面測試了收到一條創(chuàng)建信息后實例化一個Cargo的簡單場景,要求創(chuàng)建后的Cargo的標識id跟信息里的一致,并且出貨的日期一致。這個測試驅(qū)動出來一個Interface的Api::CreateCargo。
下面是另外一個測試推遲delay的場景,同樣我們看到了驅(qū)動出的Api::Delay的實現(xiàn)。
TEST(bc_demo_test, delay_cargo)
{
api::Api* api = createApi();
api::CreateCargoMsg* msg = new api::CreateCargoMsg();
msg->Id = ID;
msg->AfterDays = AFTER_DAYS;
api->CreateCargo(msg);
api->Delay(ID,2);
EXPECT_EQ(ID, provider->cargo_id);
EXPECT_EQ(12, provider->after_days);
}
長期以來對于TDD這個實踐大家都有架構設計上的疑惑,很多資深架構師擔心完全從業(yè)務需求驅(qū)動出實現(xiàn)沒法形成有效的技術架構,而且每次實現(xiàn)的重構成本都可能很高。DDD的引入從某種程度上解決了這個顧慮,通過前期的戰(zhàn)略和戰(zhàn)術建模確定了核心領域架構,這個架構是通過預先綜合討論決策的,考慮了更廣闊的業(yè)務問題,較之TDD應用的業(yè)務需求層面更加宏觀。在已有核心模型基礎上我們也會發(fā)現(xiàn)測試用例的設計更容易從應用視角出發(fā),從而降低了測試設計的難度。
關于預先設計
如果沒有讀戰(zhàn)略篇直接看本文的讀者肯定會提出關于預先設計的顧慮,畢竟DDD是被敏捷開發(fā)圈子認可的一種架構方式,其目標應該是構建架構模型的響應力。而這里給大家的更多是模式化的實現(xiàn)過程,好似從建模到代碼一切都預先設計好了。
值得強調(diào)的是,我們?nèi)匀环磳η捌谠O計的大而全(Big-Design-Up-Front,BDUF)。
但我們應該認可前期對核心領域模型的分析和設計,這樣能夠幫助我們更快地響應后續(xù)的業(yè)務變化(即在核心模型之上的應用)。這不代表著核心領域模型未來會一成不變,或者不能改變,而是經(jīng)過統(tǒng)一建模的核心部分變化頻率較之外部應用會低很多。如果核心領域模型也變化劇烈,那么我們可能就要考慮是否業(yè)務發(fā)生了根本性的變化,需要建立新的模型。
另外不能忘記我們預先定義的模型也是被局限在一個分解出來的核心問題域里的,也就是說我們并不希望一口氣把整個復雜的業(yè)務領域里的所有模型都建立起來。這種范圍的局限某種程度上也限制了我們預先設計的范圍,促使我們更多用迭代的方式來看待建模工作本身。
最后顯然我們應該有一個核心團隊來守護核心領域模型,這不代表著任何模型的設計和改動都必須由這個團隊的人做出(雖然有不少的團隊確實是這樣落地DDD的)。我們期望的是任何對核心模型的改動都能夠通過這個核心團隊來促進更大范圍的交流和溝通。檢驗一個模型是否落地的唯一標準是應用這個模型的團隊能否就模型本身達成共識。在這點上我們看到很多團隊持續(xù)通過代碼走查(code
review)的方式在線上和線下實踐基于核心模型的交流,從而起到了真正意義上的“守護”作用,讓模型本身成為團隊的共同責任。
實踐DDD時仍然需要遵循“模型是用來交流的”的這一核心原則。我們希望本文介紹的方法及模式能夠幫助大家更容易地交流領域模型,也算是對DDD戰(zhàn)略和戰(zhàn)術設計的一點補充。
另外,12.8-9我們將舉辦一場DDD China大會,我們也希望通過第一屆DDD China建立起一個架構設計人員的交流平臺。期待更多的中國技術人員能夠通過這個平臺和世界一流架構大師們建立起溝通的渠道,不僅在戰(zhàn)略層面,也在戰(zhàn)術層面和所有人一起分享討論關于DDD的一切。
文/ThoughtWorks肖然
編輯于 2017-11-09