DDD實(shí)戰(zhàn)篇:分層架構(gòu)的代碼結(jié)構(gòu)

同于其它的架構(gòu)方法艺玲,領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)DDD(Domain Driven Design)提出了從業(yè)務(wù)設(shè)計(jì)到代碼實(shí)現(xiàn)一致性的要求鞠抑,不再對分析模型和實(shí)現(xiàn)模型進(jìn)行區(qū)分饭聚。也就是說從代碼的結(jié)構(gòu)中我們可以直接理解業(yè)務(wù)的設(shè)計(jì)碍拆,命名得當(dāng)?shù)脑挘浅绦蛉藛T也可以“讀”代碼感混。

然而在整個(gè)DDD的建模過程中礼烈,我們更多關(guān)注的是核心領(lǐng)域模型的建立,我們認(rèn)為完成業(yè)務(wù)的需求就是在領(lǐng)域模型上的一系列操作(應(yīng)用)庭呜。這些操作包括了對核心實(shí)體狀態(tài)的改變,領(lǐng)域事件的存儲(chǔ)募谎,領(lǐng)域服務(wù)的調(diào)用等阴汇。在良好的領(lǐng)域模型之上数冬,實(shí)現(xiàn)這些應(yīng)用應(yīng)該是輕松而愉快的搀庶。

筆者經(jīng)歷過很多次DDD的建模工作坊铜异,在經(jīng)歷了數(shù)天一輪又一輪激烈討論和不厭其煩的審視之后秸架,大家欣慰地看著白板上各種顏色紙貼所展示出來的領(lǐng)域模型,成就感寫滿大家的臉龐东抹。就在這個(gè)大功告成的時(shí)刻,往往會(huì)有人問:這個(gè)模型我們怎么落地呢缭黔?然后大家臉上的愉悅消失了,換上了對細(xì)節(jié)就是魔鬼的焦慮试浙。但這是我們不可避免的實(shí)現(xiàn)細(xì)節(jié),DDD的原始方法論中雖然給出了“分層架構(gòu)”(Layered Architecture)的元模型田巴,但如何分層卻沒有明確定義。

分層架構(gòu)

在DDD方法提出后的數(shù)年里抄伍,分層架構(gòu)的具體實(shí)現(xiàn)也經(jīng)歷了幾代演進(jìn),直到Martin Fowler提煉出下圖的分層實(shí)現(xiàn)架構(gòu)后管宵,才逐步為大家所認(rèn)可。DDD的方法也得到了有效的補(bǔ)充箩朴,模型落地的問題也變得更容易,核心領(lǐng)域模型的范圍也做出了比較明確的定義:包括了Domain炸庞,Service Layer和Repositories。

(Martin Fowler總結(jié)提出的分層架構(gòu)實(shí)現(xiàn)查牌,注意“Resources”是基于RESTful架構(gòu)的抽象滥壕,我們也可以理解為更通用的針對外界的接口Interface纸颜。而HTTP Client主要是針對互聯(lián)網(wǎng)的通信協(xié)議绎橘,Gateways實(shí)際才是交換過程中組裝信息的邏輯所在。)

我們的核心實(shí)體(Entity)和值對象(Value Object)應(yīng)該在Domain層,定義的領(lǐng)域服務(wù)(Domain Service)在Service Layer牵敷,而針對實(shí)體和值對象的存儲(chǔ)和查詢邏輯都應(yīng)該在Repositories層法希。值得注意的是枷餐,不要把Entity的屬性和行為分離到Domain和Service兩層中去實(shí)現(xiàn)苫亦,即所謂的貧血模型,事實(shí)證明這樣的實(shí)現(xiàn)方式會(huì)造成很大的維護(hù)問題屋剑。DDD戰(zhàn)術(shù)建模中的元模型定義不應(yīng)該在實(shí)現(xiàn)過程中被改變,作為元模型中元素之一的實(shí)體本身就應(yīng)該包含針對自身的行為定義孕讳。

基于這個(gè)模型,下面我們來談?wù)劯唧w的代碼結(jié)構(gòu)厂财。對于這個(gè)分層架構(gòu)還有疑惑的讀者可以精讀一下Martin的原文。有意思的一點(diǎn)是峡懈,這個(gè)模型的敘述實(shí)際是在微服務(wù)架構(gòu)的測試文章中,其中深意值得大家體會(huì)肪康。

這里需要明確的是,我們談?wù)摯a結(jié)構(gòu)的時(shí)候磷支,針對的是一個(gè)經(jīng)過DDD建模后的子問題域(參見戰(zhàn)略設(shè)計(jì)篇),這是我們明確的組件化邊界嗤栓。是否進(jìn)一步組件化,比如按照限界上下文(Bounded Context)模塊化,或采用微服務(wù)架構(gòu)服務(wù)化叨叙,核心實(shí)體都是進(jìn)一步可能采用的組件化方法。從抽象層面講擂错,老馬提煉的分層架構(gòu)適用于面向業(yè)務(wù)的服務(wù)化架構(gòu),所以如果要進(jìn)一步組件化也是可以按照這個(gè)代碼結(jié)構(gòu)來完成的剑鞍。

總體的代碼目錄結(jié)構(gòu)如下:

- DDD-Sample/src/
    domain
    gateways
    interface
    repositories
    services

這個(gè)目錄結(jié)構(gòu)一一對應(yīng)了前文的分層架構(gòu)圖昨凡。完整的案例代碼請從GitHub下載蚁署。

可以看到實(shí)際上我們并沒有建立外部存儲(chǔ)(Data Mappers/ORM)和對外通信(HTTP Client)的目錄。從領(lǐng)域模型和應(yīng)用的角度光戈,這兩者都是我們不必關(guān)心的,能夠驗(yàn)證整個(gè)領(lǐng)域模型的輸入和輸出就足夠了晌杰。至于什么樣的外部存儲(chǔ)和外部通信機(jī)制是可以被“注入”的筷弦。這樣的隔離是實(shí)現(xiàn)可獨(dú)立部署服務(wù)的基礎(chǔ)肋演,也是我們能夠測試領(lǐng)域模型實(shí)現(xiàn)的要求烂琴。

模型表達(dá)

根據(jù)分層架構(gòu)確立了代碼結(jié)構(gòu)后,我們需要首先定義清楚我們的模型边灭。如前面講到的,這里主要涉及的是從戰(zhàn)術(shù)建模過程中得到的核心實(shí)體和服務(wù)的定義绒瘦。我們利用C++頭文件(.h文件)來展示一個(gè)Domain模型的定義扣癣,案例靈感來源于DDD原著里的集裝箱貨運(yùn)例子惰帽。

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;
};
}

這個(gè)實(shí)現(xiàn)首先申明了元模型實(shí)體Entity和值對象ValueObject父虑。實(shí)體一定會(huì)有一個(gè)標(biāo)識(shí)id。在實(shí)體的基礎(chǔ)上聲明了DDD中的重要元素聚合根 AggregateRoot呜魄。根據(jù)定義,聚合根本身就應(yīng)該是一個(gè)實(shí)體爵嗅,所以AggregateRoot繼承了Entity笨蚁。

這個(gè)案例中我們定義了一個(gè)實(shí)體Cargo睹晒,同時(shí)也是一個(gè)聚合根。Delivery是一個(gè)值對象戚啥。雖然這里為了實(shí)現(xiàn)效率采用的是struct,在C++里可以理解為定義一個(gè)class類猫十。

依賴關(guān)系

代碼目錄結(jié)構(gòu)并不能表達(dá)分層體系中各層的依賴關(guān)系键痛,比如Domain層是不應(yīng)該依賴于其它任何一層的炫彩。維護(hù)各層的依賴關(guān)系是至關(guān)重要的絮短,很多團(tuán)隊(duì)在實(shí)施的過程中都沒有能夠建立起這樣的工程紀(jì)律,最后造成代碼結(jié)構(gòu)的混亂丁频,領(lǐng)域模型也被打破。

根據(jù)分層架構(gòu)的規(guī)則席里,我們可以看到示例中的代碼結(jié)構(gòu)如下圖。

Domain是不依賴于任何的其它對象的改基。Repositories是依賴于Domain的咖为,實(shí)現(xiàn)如下:引用了model.h。

#include "model.h"
#include <vector>

using namespace domain;

namespace repositories {
struct Repository
{
};
...

Services是依賴于Domain和Repositories的躁染,實(shí)現(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 {
    ... ...
};
...

為了維護(hù)合理的依賴關(guān)系,依賴注入(Depedency Injection)是需要經(jīng)常采用的實(shí)現(xiàn)模式吞彤,它作為解耦合的一種方法相信大家都不會(huì)陌生,具體定義參見這里饰恕。

在測試構(gòu)建時(shí),我們利用了一個(gè)IoC框架(依賴注入的實(shí)現(xiàn))來構(gòu)造了一個(gè)Api褥赊,并且把相關(guān)的依賴(如CargoService)注入給了這個(gè)Api莉恼。這樣既沒有破壞Interface和Service的單向依賴關(guān)系拌喉,又解決了測試過程中Api的實(shí)例化要求俐银。

auto provider = std::make_shared< StubCargoProvider >();

api::Api* createApi()  {
    ContainerBuilder builder;
    builder.registerType< CargoRepository >().singleInstance();
    builder.registerInstance(provider).as<CargoProvider>();
    builder.registerType< CargoService >().singleInstance();
    builder.registerType<api::Api>().singleInstance();

    auto container = builder.build();

    std::shared_ptr<api::Api> api = container->resolve<api::Api>();

    return api.get();
}

測試實(shí)現(xiàn)

有了領(lǐng)域模型,大家自然會(huì)想著如何去實(shí)現(xiàn)業(yè)務(wù)應(yīng)用了田藐,而實(shí)現(xiàn)應(yīng)用的過程中一定會(huì)考慮到單元測試的設(shè)計(jì)。在構(gòu)建高質(zhì)量軟件過程中汽久,單元測試已經(jīng)成為了標(biāo)準(zhǔn)規(guī)范踊餐,但高質(zhì)量的單元測試卻是困擾很多團(tuán)隊(duì)的普遍問題景醇。很多時(shí)候設(shè)計(jì)測試比實(shí)現(xiàn)應(yīng)用本身更加困難吝岭。

這里很難有一個(gè)固定標(biāo)準(zhǔn)來評判某個(gè)時(shí)間點(diǎn)的單元測試質(zhì)量,但一個(gè)核心的原則是讓用例盡量測試業(yè)務(wù)需求而不是實(shí)現(xiàn)方式本身窜管。滿足業(yè)務(wù)需求是我們的目標(biāo),實(shí)現(xiàn)方式可能有多種幕帆,我們不希望需要持續(xù)重構(gòu)的實(shí)現(xiàn)代碼影響到我們的測試用例。比如針對實(shí)現(xiàn)過程中的某個(gè)函數(shù)進(jìn)行入?yún)⒑统鰠⒌膯卧獪y試常熙,當(dāng)這個(gè)函數(shù)發(fā)生一點(diǎn)改變(即使是重命名)仗扬,我們也需要改動(dòng)測試症概。

測試驅(qū)動(dòng)開發(fā)TDD無疑是一種好的實(shí)踐早芭,如果應(yīng)用得當(dāng),它確實(shí)能夠?qū)崿F(xiàn)我們上述的原則退个,并且能夠幫助我們交流業(yè)務(wù)的需求。比較有意思的是舱馅,在基于DDD建立的核心模型之上應(yīng)用TDD似乎更加順理成章。類比DDD和TDD雖然是不恰當(dāng)?shù)拇停覀儠?huì)發(fā)現(xiàn)兩者在遵循的原則上是一致的,即都是面向業(yè)務(wù)做分解和設(shè)計(jì):DDD就整個(gè)業(yè)務(wù)問題域進(jìn)行了分解干毅,形成子問題域;TDD就業(yè)務(wù)需求在實(shí)現(xiàn)時(shí)進(jìn)行任務(wù)分解硝逢,從簡單場景到復(fù)雜場景逐步通過測試驅(qū)動(dòng)出實(shí)現(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)建信息后實(shí)例化一個(gè)Cargo的簡單場景,要求創(chuàng)建后的Cargo的標(biāo)識(shí)id跟信息里的一致憨奸,并且出貨的日期一致猎拨。這個(gè)測試驅(qū)動(dòng)出來一個(gè)Interface的Api::CreateCargo膀藐。

下面是另外一個(gè)測試推遲delay的場景红省,同樣我們看到了驅(qū)動(dòng)出的Api::Delay的實(shí)現(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這個(gè)實(shí)踐大家都有架構(gòu)設(shè)計(jì)上的疑惑吧恃,很多資深架構(gòu)師擔(dān)心完全從業(yè)務(wù)需求驅(qū)動(dòng)出實(shí)現(xiàn)沒法形成有效的技術(shù)架構(gòu),而且每次實(shí)現(xiàn)的重構(gòu)成本都可能很高傲醉。DDD的引入從某種程度上解決了這個(gè)顧慮,通過前期的戰(zhàn)略和戰(zhàn)術(shù)建模確定了核心領(lǐng)域架構(gòu)硬毕,這個(gè)架構(gòu)是通過預(yù)先綜合討論決策的礼仗,考慮了更廣闊的業(yè)務(wù)問題,較之TDD應(yīng)用的業(yè)務(wù)需求層面更加宏觀元践。在已有核心模型基礎(chǔ)上我們也會(huì)發(fā)現(xiàn)測試用例的設(shè)計(jì)更容易從應(yīng)用視角出發(fā),從而降低了測試設(shè)計(jì)的難度单旁。

關(guān)于預(yù)先設(shè)計(jì)

如果沒有讀戰(zhàn)略篇直接看本文的讀者肯定會(huì)提出關(guān)于預(yù)先設(shè)計(jì)的顧慮,畢竟DDD是被敏捷開發(fā)圈子認(rèn)可的一種架構(gòu)方式象浑,其目標(biāo)應(yīng)該是構(gòu)建架構(gòu)模型的響應(yīng)力琅豆。而這里給大家的更多是模式化的實(shí)現(xiàn)過程死嗦,好似從建模到代碼一切都預(yù)先設(shè)計(jì)好了。

值得強(qiáng)調(diào)的是越除,我們?nèi)匀环磳η捌谠O(shè)計(jì)的大而全(Big-Design-Up-Front外盯,BDUF)。 但我們應(yīng)該認(rèn)可前期對核心領(lǐng)域模型的分析和設(shè)計(jì)饱苟,這樣能夠幫助我們更快地響應(yīng)后續(xù)的業(yè)務(wù)變化(即在核心模型之上的應(yīng)用)。這不代表著核心領(lǐng)域模型未來會(huì)一成不變箱熬,或者不能改變,而是經(jīng)過統(tǒng)一建模的核心部分變化頻率較之外部應(yīng)用會(huì)低很多蚤认。如果核心領(lǐng)域模型也變化劇烈,那么我們可能就要考慮是否業(yè)務(wù)發(fā)生了根本性的變化砰琢,需要建立新的模型良瞧。

另外不能忘記我們預(yù)先定義的模型也是被局限在一個(gè)分解出來的核心問題域里的陪汽,也就是說我們并不希望一口氣把整個(gè)復(fù)雜的業(yè)務(wù)領(lǐng)域里的所有模型都建立起來褥蚯。這種范圍的局限某種程度上也限制了我們預(yù)先設(shè)計(jì)的范圍,促使我們更多用迭代的方式來看待建模工作本身赞庶。

最后顯然我們應(yīng)該有一個(gè)核心團(tuán)隊(duì)來守護(hù)核心領(lǐng)域模型,這不代表著任何模型的設(shè)計(jì)和改動(dòng)都必須由這個(gè)團(tuán)隊(duì)的人做出(雖然有不少的團(tuán)隊(duì)確實(shí)是這樣落地DDD的)舍哄。我們期望的是任何對核心模型的改動(dòng)都能夠通過這個(gè)核心團(tuán)隊(duì)來促進(jìn)更大范圍的交流和溝通。檢驗(yàn)一個(gè)模型是否落地的唯一標(biāo)準(zhǔn)是應(yīng)用這個(gè)模型的團(tuán)隊(duì)能否就模型本身達(dá)成共識(shí)表悬。在這點(diǎn)上我們看到很多團(tuán)隊(duì)持續(xù)通過代碼走查(code review)的方式在線上和線下實(shí)踐基于核心模型的交流丧靡,從而起到了真正意義上的“守護(hù)”作用籽暇,讓模型本身成為團(tuán)隊(duì)的共同責(zé)任。

實(shí)踐DDD時(shí)仍然需要遵循“模型是用來交流的”的這一核心原則戒悠。我們希望本文介紹的方法及模式能夠幫助大家更容易地交流領(lǐng)域模型,也算是對DDD戰(zhàn)略和戰(zhàn)術(shù)設(shè)計(jì)的一點(diǎn)補(bǔ)充绸狐。

作者:ThoughtWorks肖然

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末累盗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子若债,更是在濱河造成了極大的恐慌,老刑警劉巖蠢琳,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蓝牲,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)搞旭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進(jìn)店門菇绵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人咬最,你說我怎么就攤上這事∮牢冢” “怎么了?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵圈驼,是天一觀的道長望几。 經(jīng)常有香客問我绩脆,道長,這世上最難降的妖魔是什么靴迫? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任玉锌,我火速辦了婚禮名挥,結(jié)果婚禮上主守,老公的妹妹穿的比我還像新娘。我一直安慰自己参淫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著民效,像睡著了一般。 火紅的嫁衣襯著肌膚如雪畏邢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天程储,我揣著相機(jī)與錄音,去河邊找鬼章鲤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛败徊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播皱蹦,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼眷蜈,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了酌儒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嫌拣,失蹤者是張志新(化名)和其女友劉穎柔袁,沒想到半個(gè)月后异逐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡灰瞻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了燎竖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片要销。...
    茶點(diǎn)故事閱讀 40,498評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖疏咐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情浑塞,我是刑警寧澤,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布掏愁,位于F島的核電站,受9級(jí)特大地震影響果港,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜京腥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一溅蛉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧船侧,春花似錦、人聲如沸镜撩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽憔古。三九已至淋袖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間即碗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工内舟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人验游。 一個(gè)月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓保檐,卻偏偏與公主長得像批狱,于是被迫代替她去往敵國和親展东。 傳聞我的和親對象是個(gè)殘疾皇子炒俱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容