端口和適配器架構——DDD好幫手

摘要

  • 本文源自2018領域驅動設計中國峰會《領域驅動設計與演進式架構專題》的Session之一,是其博客版
  • 在實踐領域驅動設計時,可以挑選一些方法互為參照,端口和適配器架構概念簡單,容易掌握,適合作為實踐領域驅動設計的輔助方法。

大概一個月前,在做2018年領域驅動設計大會預告的時候,上一屆大會的主題演講者肖然提出這樣的擔憂:工具和方法似乎沒有很好地解決“落地難”的挑戰

  1. 沒有一套方法能夠打遍天下,具體到采用哪一種方案,仿佛都需要增加一個定語“這取決于……”。
  2. 不管是在DDD原著,還是后續不少專家的書籍中,都暗示、甚至明示架構設計的終極大招還是By Experience ——靠經驗吃飯。
  3. 從戰略角度的子領域劃分,到戰術建模層面實體、值對象的選擇,最終的決策很可能不是完全“理性”的,經驗這個“感性”的東西發揮著很大的作用”

所以,推動領域驅動設計實踐的方向是否應該從介紹方法轉變為介紹如何累積經驗?

看了這篇文章后,我放棄了之前準備的話題《CQRS和Event Sourcing,從入門到放棄》,因為可能你一年都不會遇到一個需要使用這兩種方法才能解決的復雜項目。

如何快速獲取經驗?無非就是多練,但是練了要討論和總結,我遇到過這樣的對話,我將它稱為”兩小兒辯DDD“:

A: 我覺得你這里不該使用實體,應該使用值對象

B: 我覺得你這個接口不是領域服務,它其實是應用服務,你這樣做不DDD

A: 你的實體不應該調用Repository,你這樣做也不DDD

B: (看著我)你來評評理,我們誰說的對

我:俺也不知道,這取決于...

這樣的復盤方式效果欠佳,我建議不妨從DDD中跳出,找一種方法互為參照和檢驗,比如”端口和適配器架構“

什么是端口和適配器架構

套用流行的提問方式:當我們在說架構時,我們在說什么?在本文中我們不是在討論微服務架構,也不是討論基礎設施架構,這里的架構指:

  1. 在單個應用(進程)中
  2. 代碼是如何組織起來實現一個端到端的用戶請求的
  3. 它與框架無關,不管你是使用ORM框架或是JDBC,這不是架構的關鍵差異點

一個例子是三層架構,展現層負責接收用戶指令、渲染視圖;業務邏輯層負責處理"業務邏輯";數據層負責和數據庫打交道,保存和讀取數據。

“經典”的三層架構

三層(或多層)架構仍然是目前最普遍的架構,但它也有缺點:

  1. 架構被過分簡化,如果解決方案中包含發送郵件通知,代碼應該放置在哪些層里?
  2. 它雖然提出了業務邏輯隔離,但沒有明確的架構元素指導我們如何隔離

因此,在實際落地時,業務邏輯容易泄漏到展示層中,導致當應用需要一種新的使用方式時(例如開放API),原有的業務邏輯層可能不能快速重用,同樣的問題也發生在數據層和業務邏輯層之間。

那么有沒有替代的方案?Alistair Cockburn是敏捷運動的早期推動者之一,他于2005年在其博客中提出了端口和適配器架構,他對該架構的一句話定義是:

"應用應能平等地被用戶、其他程序、自動化測試或腳本驅動,也可以獨立于其最終的運行時設備和數據庫進行開發和測試"

原文為“Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.”

該架構由端口和適配器組成,所謂端口是應用的入口和出口,在許多語言中,它以接口的形式存在。例如以取消訂單為例,“發送訂單取消通知”可以被認為是一個出口端口,訂單取消的業務邏輯決定了何時調用該端口,訂單信息決定了端口的輸入,而端口為預訂流程屏蔽了通知發送方式的實現細節。

而適配器分為兩種,主適配器(別名Driving Adapter)代表用戶如何使用應用,從技術上來說,它們接收用戶輸入,調用端口并返回輸出。Rest API是目前最常見的應用使用方式,以取消訂單為例,該適配器實現Rest API的Endpoint,并調用入口端口CancelOrderService。同一個端口可能被多種適配器調用,例如CancelOrderService也可能會被實現消息協議的Driving Adapter調用以便異步取消訂單。

次適配器(別名Driven Adapter)實現應用的出口端口,向外部工具執行操作,例如

  • 向MySQL執行SQL,存儲訂單
  • 使用Elasticsearch的API搜索產品
  • 使用郵件/短信發送訂單取消通知

若將其可視化,Driving Adapter和Driven Adapter基于端口圍繞著應用形成左右結構,有別于傳統的分層形象,形成一個六邊形,因此也會稱作六邊形架構

可視化端口和適配器架構

如果到此我已經成功地把你講暈了,請不要擔心,我們接下來通過一個案例體驗一下這個架構。

端口和適配器架構有什么好處

DDD郵輪,有咨詢公司的報告顯示,在接下來的幾年內,郵輪游作為國人出游形式的比例會大幅上升,在這樣一個大背景下,DDD Cruise,一家中國的郵輪公司,正在研發新一代的預訂系統,嘗試在線郵輪預訂。

目前計劃中有兩個觸點應用:

  1. 微信小程序——提供郵輪搜索、郵輪預訂的核心體驗
  2. 中國區官網——這原是一個包含幾個HTML頁面的遺留應用,本次希望可以提供郵輪搜索的功能,值得注意的是,有部分郵輪是承包給旅行社銷售,在網站上也需要展示以便做市場宣傳

C4 Model——System Context Diagram

在這兩個觸點背后,是這次的主角,預訂引擎1.0,計劃以一個單體應用起步,為觸點應用提供API,實現郵輪搜索、郵輪預訂。郵輪有多個數據來源,一部分來自一個遺留的預訂系統,一部分來自業務部門的Excel表格,存放在AWS S3對象存儲中。最后還有一個小型的Headless CMS為市場人員提供郵輪描述,吸引眼球。

現在讓我們代入端口和適配器:

上“套路”,Driving Adapter一個,端口兩個,Driven Adapter兩個,連線少許

  1. API Controller是一個典型的Driving Adapter,它實現Rest API的Endpoint,調用入口端口CruiseSearch
  2. CruiseSearch作為應用的入口,向Driving Adapter屏蔽了郵輪搜索的實現。
  3. 在另一邊,出口端口CruiseSource要求返回全量的Cruise數據,為應用隱藏了外部數據源的集成方案:從遺留預訂系統或AWS S3上的文件中抽取Cruise

促進單一職責原則

那么我們接下來在這個架構的基礎上,進行概要設計,組件很自然地分為了三個部分:

概要設計類圖

  1. 綠色是Driving Adapter,如果你對Java-Spring技術棧,可以從命名發現他是一個RestController

  2. 黃色是Cruise Search的實現,這里的概念只和郵輪相關,你在這里不應該看到技術術語

  3. 粉色部分則是Driven Adapter,除了與處理從數據源獲取Cruise的Adapter,我們還需要

    a. CompositeCruiseSource,它不直接與數據源打交道,但它負責合并多個數據源并根據規則去除重復的Cruise
    b. CachingCruiseSource,它也不直接與數據源打交道,負責緩存Cruise

從架構角度來看,這些組件很簡單。請注意,簡單(Simple)并不代表著容易(Easy),簡單說的是只做一件事(或一種事),而容易是指做一件事的難度,例如如果使用Spring MVC實現Driving Adapter,利用注解寥寥幾行代碼就可以實現。由于這些組件要么實現業務邏輯,要么實現對某種技術的適配,符合單一職責原則,你可以更有效地將變更控制在某一個范圍內,更有信心地應對變化。

澄清測試策略

應對變化的另一個有效手段是自動化測試,測試金字塔是最常被提及的測試策略,它建議自動化測試集應該由大量單元測試作為基礎,它們編寫容易、運行速度快,應該只包含少量的用UI驅動的測試,由于需要處理測試數據沖突、外部依賴準備,它們編寫困難、運行速度也較慢。但中層的service/集成測試的測試目標是什么,它們和單元測試有什么區別呢?


測試金字塔——端口和適配器版

如果你也有此困惑,不妨按照端口和適配器架構來重新解讀,金字塔應該包含大量的Driving Adatper測試、業務邏輯測試、Driven Adapter測試。

  1. Driving Adapter測試,目標是驗證API能正確地解析輸入、按預期的參數調用了入口端口并生成輸出。由于Driving Adapter不關心入口端口的實現,在測試中,可以通過Mock方便地構造測試場景,并提升測試速度。


近兩年開始流行的契約測試也可以認為是Driving Adapter測試的擴展

  1. 業務邏輯測試,通過Mock出口端口,同樣可以方便地構造測試數據,而且這里應該都是Plain Object,測試可以完全在內存中運行,速度是最快的


傳統的單元測試

  1. Driven Adapter測試,目標是驗證按預期的方式操作了外部工具、下游服務、數據庫。傳統上,涉及這些外部依賴的測試編寫難度大,運行速度慢,但如果出口端口和Driven Adapter設計得當,它們就不涉及業務邏輯,從而需要測試用例會大大減少,通過引入內存數據庫、Stub Server等技術,其測試場景的構建難度會改善不少,整體執行時間也會相應減少


單一職責的Driven Adapter也降低了測試難度,不過測試速度仍然相對較慢

需要注意的是以上測試都是在技術上檢測組件是否符合預期,可以考慮適當加入E2E Test來驗證這些組件集成起來可用,業務上符合預期,一般覆蓋關鍵功能的Happy Path場景即可。

促進增量開發

端口和適配器架構可能還能給與我們一些靈感,實施增量開發,不妨看一下這個用戶故事分解的例子:

由于旅行社代售的郵輪都來自于Excel表格,只要確定了表格字段含義,我們就可以開始集成,我們選擇這張卡來搭建腳手架:


如果業務優先級允許,選擇技術實現最簡單的卡搭建腳手架

接下來在InMemoryCruiseSearch中實現篩選:


實現篩選功能

引入LegacyBookingCruiseSource和CompositeCruiseSource


擴展數據源,另外還需要擴展Cruise銷售渠道的篩選條件實現

最后,可以引入一張技術卡:


加入CachingCruiseSource,提升Cruise讀取速度

到這里,我們不妨小結一下:


端口和適配器架構的組成元素及它的好處

與領域驅動設計的協同增效

由于概念簡單、易于掌握,端口和適配器架構很適合作為DDD的入門輔導工具,而領域驅動設計的諸多方法也能夠補充端口和適配器架構的空白,形成合力。


端口和適配器架構與領域驅動設計的協同增效

校驗“通用語言”

通用語言是領域驅動設計的核心精髓,它建議各方(無論是領域專家和還是開發人員)對于同一件事都使用相同的詞匯。這可以防止各方在溝通領域問題、制定解決方案時不會由于不同的專業背景產生誤解,最終促進了識別正確的問題,采用正確的解決方案。甚至有激進的觀點認為“領域模型就是通用語言本身”。

端口和適配器雖然不能直接幫助我們找到領域模型或通用語言,但它有助于我們從通用語言中快速剔除技術概念:凡是用于實現適配器的技術細節都應該被排除。讓我們回到DDD Cruise的例子:


對話片段,注意綠色字體都和Driven Adapter有關,它們應該被通用語言排除

作為“DDD戰術設計”的腳手架

領域驅動設計于2004年橫空出世,一年后端口和適配器被提出,在戰術設計層面,我們可以發現諸多相似點,互為呼應。以架構為例,DDD原著中提出的架構很有意思:乍看之下,以為是傳統的分層架構,但卻強調了Infrastructure對各層的實現。


如果我們做一下職責分析,你會發現這不就是端口和適配器嘛?

端口和適配器的優勢是突出了分層不是重點,技術實現隔離才是關鍵,讓你不再糾結是否允許組件跨層調用。而DDD原著架構的優勢是用Application和Domain進一步澄清了業務邏輯這個模糊的概念。不妨合二為一:


值得一提的是,Application和Domain甚至可以是聲明式的,作為端口存在,例如DDD構建塊中的ApplicationService是一個典型的入口端口,而Repository則是一個典型的出口端口。

讓我們回到DDD Cruise,細化Cruise的領域模型:CruiseSearch(應用服務),但實際的篩選邏輯會交給Cruise(實體)及其值對象ItineraryLeg實現,你甚至可以引入DDD書中提到的規格模式,進一步強化單一職責,將篩選條件與領域模型篩選方法的映射工作從InMemoryCruiseSearch中剝離,使其完全只負責步驟協調


將應用服務、領域模型代入Cruise Search

讓“DDD戰略設計”指導隔離實施

實施戰略設計時候,有一個重要的實踐是限界上下文的識別,當存在多個限界上下文的時候,很有可能需要集成,防腐層是常見的集成手段。 來看這個示意:Service A 是左側限界上下文暴露出來的接口,通過適配器調用右側限界上下文的接口。


“防腐層”

這是不是很眼熟?這不正是端口和Driven Adapter嗎?你可以認為它們是一種特化的防腐層。 那么當一個單體應用中有多個限界上下文時,它們之間也應該用端口隔離,用適配器集成。如果你使用微服務來隔離限界上下文,端口和適配器架構則適用于其中每個服務。

回到DDD Cruise,還記得我們需要集成Headless CMS嗎,由于在當前階段,我們工作在單體應用中,CruiseSearch的API需要返回包含郵輪描述的信息。


沒有識別限界上下文,雖然引入了端口和Driven Adapter,但不夠理想

一種方案是將這些描述信息加入到領域模型中,由于已有的兩個數據源都無法提供這些信息,我們又引入了ContentfulCruiseSource及另一個出口端口CruiseContentEnricher及其Driven Adapter以便填充這些信息。但這個方案不夠理想:

  1. 在郵輪搜索的篩選實現中,描述信息并沒有實際作用,領域模型變得更臃腫了,甚至造成了干擾
  2. 在郵輪搜索的測試中,我們并不關心這些描述信息,但卻可能需要構造一些Dummy數據,避免可能的空指針誤報


將限界上下文引入DDD Cruise

在限界上下文概念指導下的另一種方案,引入CruiseContentEnricher既作為入口端口、同時也作為出口端口,保持郵輪搜索上下文不被干擾,這個方案的好處是,假設郵輪搜索引擎進行微服務改造,很有可能將描述信息填充的職責分離到單獨的服務中去,這時,只需要再提供一個輸入、輸出不含描述信息的Driving Adapter就可以了。


在限界上下文指導下找到更穩定的端口

總結

我們介紹了端口和適配器架構,它簡單易掌握,和領域驅動設計又合拍,希望它能幫助你快速積累DDD經驗!


文/ThoughtWorks周宇剛

更多精彩洞見,請關注微信公眾號:ThoughtWorks洞見

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容