原文:https://herbertograca.com/2019/06/05/reflecting-architecture-and-domain-in-code/
這篇文章是軟件架構(gòu)編年史(譯)的一部分,這部編年史由一系列關(guān)于軟件架構(gòu)的文章組成沦零。在這一系列文章中闻坚,我將寫下我對軟件架構(gòu)的學習和思考,以及我是如何運用這些知識的锄俄。如果你閱讀了這個系列中之前的文章,本篇文章的的內(nèi)容將更有意義勺拣。
在創(chuàng)建應(yīng)用的時候奶赠,讓它可以工作易如反掌。要讓它在處理大量數(shù)據(jù)的情況下仍然保持性能药有,會有點困難毅戈。但是最難的挑戰(zhàn)是構(gòu)建一個真正可以維系多年(十年、二十年甚至一百年)的應(yīng)用程序愤惰。
我工作過的大多數(shù)公司都有每三到五年就重建應(yīng)用的歷史苇经,有時甚至不到兩年就要重建。這種做法成本極高宦言,它將極大地影響應(yīng)用程序的成功扇单,進而極大地影響公司的成功,還會讓開發(fā)人員在亂成一鍋粥的代碼庫種凌亂奠旺,讓他們想萌生辭職的想法蜘澜。任何一家志向遠大的正經(jīng)公司施流,都無法承受任何經(jīng)濟上、時間上鄙信、聲譽上瞪醋、客戶上、人才上的損失扮碧。
讓應(yīng)用程序保持可維護性的基石是讓代碼能夠反映出架構(gòu)和領(lǐng)域趟章,這對防止所有棘手問題至關(guān)重要。
清晰架構(gòu)是對比我經(jīng)驗更豐富的開發(fā)者所倡導的原則以及實踐的合理解釋慎王,也是對我如何組織代碼庫使之反映出項目架構(gòu)與領(lǐng)域并便于溝通的經(jīng)驗總結(jié)蚓土。
在我的上一篇博客(譯文)里,我將這些理念匯總起來并用信息圖和 UML 圖呈現(xiàn)赖淤,試圖建立我思考的概念圖譜蜀漆。
然而,我們怎樣才能把實踐落實到代碼中呢咱旱?
在這篇博客里确丢,我將說明我是如何在代碼中體現(xiàn)一個項目的結(jié)構(gòu)和領(lǐng)域的,還將提出一個通用的結(jié)構(gòu)吐限,我認為它能幫助我們規(guī)劃好可維護性鲜侥。
我的兩張腦圖
在這個系列的前兩篇文章里我介紹了兩張腦圖,我用它們來思考代碼和組織代碼倉庫诸典,至少在我腦海里是這樣想的描函。
第一張腦圖由一系列同心圓層級組成,它們最終按照業(yè)務(wù)維度的應(yīng)用模塊切分狐粱,形成組件舀寓。在這張圖里,依賴的方向由外向內(nèi)肌蜻,意味著內(nèi)層對外層可見互墓,而外層對內(nèi)層不可見。
第二張則是一組平面的層級蒋搜,其中最上面的一層就是前面這張同心圓篡撵,下一層是組件之間共享的代碼(共享內(nèi)核),再下一層使是我們自己對編程語言的擴展豆挽,最下面一層則是實際使用的編程語言酸休。這里的依賴方向是自上而下的。
體現(xiàn)架構(gòu)的代碼風格
使用體現(xiàn)架構(gòu)的代碼風格祷杈,意味著代碼風格(編碼規(guī)范、類/方法/變量命名約定渗饮、代碼結(jié)構(gòu)...)某種程度上可以和閱讀代碼的人交流領(lǐng)域和架構(gòu)的設(shè)計意圖但汞。要實現(xiàn)體現(xiàn)架構(gòu)的代碼風格迟隅,主要有兩種思路迁筛。
“[…] 體現(xiàn)架構(gòu)的代碼風格能讓你給代碼的閱讀者留下提示,幫助他們正確地推斷出設(shè)計意圖。”
—George Fairbanks
第一種思路是通過代碼制品的名字(類粱挡、變量、模塊...)來傳達領(lǐng)域和架構(gòu)的含義锨苏。因此公罕,如果一個類是處理收據(jù)(Invoice)實體的倉庫(Repository),我們就應(yīng)該將它命名成InvoiceRepository
容贝,從這個名字我們就可以看出自脯,它處理的是收據(jù)領(lǐng)域的概念,而它在架構(gòu)中被當做一個倉庫斤富。這可以幫助我們理解它應(yīng)該放在哪個地方膏潮,何時使用它以及如何使用它。但是满力,我認為代碼倉庫中并不是每個代碼制品都需要這樣做焕参,例如,我覺得不必為每個實體(Entity)都加上后綴Entity
油额,這樣做就有些畫蛇添足叠纷,徒增噪音。
“[…] 代碼應(yīng)該體現(xiàn)架構(gòu)潦嘶。換句話說涩嚣,我一看到代碼,就應(yīng)該能夠清晰地區(qū)分出各種組件[…]”
—Simon Brown
第二種思路是讓代碼倉庫中的頂級制品明確地區(qū)分出各個子域衬以,即領(lǐng)域維度的模塊缓艳,也就是組件。
第一種思路應(yīng)該很清楚看峻,無需贅述阶淘。但第二種思路有點兒微妙,我們得深入探討一下互妓。
讓架構(gòu)清晰的展現(xiàn)出來
在我的第一張圖里溪窒,我們已經(jīng)看到,在最粗粒度的層級上冯勉,我們只有三種不同用途的代碼:
- 用戶界面澈蚌,這里的代碼就是為了適配某個用例的傳達機制;
- 應(yīng)用核心灼狰,這里的代碼就是用例和領(lǐng)域邏輯宛瞄;
- 基礎(chǔ)設(shè)施,這里的代碼就是為了適配應(yīng)用核心所需的工具/庫交胚。
因此份汗,在源代碼的根目錄下我們可以創(chuàng)建三個文件夾來體現(xiàn)這三類代碼盈电,一個文件夾對應(yīng)一個類別的代碼。這三個文件夾表示三個命名空間杯活,稍后我們甚至可以創(chuàng)建測試來斷言核心對用戶界面和基礎(chǔ)設(shè)施可見匆帚,反過來卻不可見,也就是說旁钧,我們可以測試由外向內(nèi)的依賴方向吸重。
用戶界面
一個 Web 企業(yè)應(yīng)用通常擁有多套 API,例如歪今,一套給客戶端使用的 REST API嚎幸,還有一套給第三方應(yīng)用使用的 web-hook, 業(yè)務(wù)還有一套需要維護的遺留 SOAP API彤委,或者還有一套給全新移動應(yīng)用使用的 GraphQL API…
這樣的應(yīng)該通常還有一些 CLI 命令鞭铆,用于定時作業(yè)(Cron Job)或按需的維護操作。
當然焦影,還有普通用戶可以使用的網(wǎng)站本身车遂,但也許還有另一個供應(yīng)用管理員使用的網(wǎng)站。
這些全都是同一個應(yīng)用的不同視圖斯辰,全都是同一個應(yīng)用的不同用戶界面舶担。
實際上我們的應(yīng)用可能擁有多個用戶界面,其中有些還是供非人類用戶(第三方應(yīng)用)使用的彬呻。我們通過文件/命名空間來區(qū)分并隔離這些用戶界面衣陶,來展現(xiàn)出這一點。
用戶界面主要有三類:API闸氮、CLI 和網(wǎng)站剪况。所以我們在UserInterface根命名空間里為每個類別創(chuàng)建一個文件夾,將不同界面的類型清晰地區(qū)分開來蒲跨。
下一步译断,如果有必要的話,我們還可以繼續(xù)深入每種類型的命名空間或悲,再創(chuàng)建更細分類的用戶界面的命名空間(CLI 可能不需要再細分了)孙咪。
基礎(chǔ)設(shè)施
和用戶界面一樣,我們的應(yīng)用使用了多種工具(庫和第三方應(yīng)用)巡语,例如 ORM翎蹈、消息隊列、SMS 提供商男公。
此外荤堪,上述每一種工具都可以有不同的實現(xiàn)。例如,考慮一家公司業(yè)務(wù)擴張到另一個國家的情況逞力,由于價格的因素曙寡,不同的國家最好采用不同的 SMS 提供商:我們需要端口相同的適配器的不同實現(xiàn),這樣使用時可以互相替換寇荧。另一個例子是對數(shù)據(jù)庫 Schema 進行重構(gòu)或者切換數(shù)據(jù)庫引擎,需要(或決定要)切換 ORM 時:我們會在應(yīng)用中注入兩種 ORM 適配器执隧。
因此揩抡,在Infrastructure命名空間來說,我們先給每一種工具類型創(chuàng)建一個命名空間(ORM镀琉、MessageQueue峦嗤、SmsClient),然后再每一種工具類型內(nèi)部為每一種用到的供應(yīng)商(Doctrine屋摔、Propel烁设、MessageBird、Twilio...)的適配器在創(chuàng)建一個命名空間钓试。
核心
在Core命名空間下装黑,可以按照最粗粒度的層級劃分出三類代碼: 組件(Component)、共享內(nèi)核(Shared Kernel) 和 端口(Port)弓熏。為這三個類別創(chuàng)建文件夾/命名空間恋谭。
組件
在 Component 命名空間下,我們?yōu)槊總€組件創(chuàng)一個命名空間挽鞠,然后在每個組件命名空間下疚颊,我們再分別為應(yīng)用(Application)層和領(lǐng)域(Domain)層分別創(chuàng)建一個命名空間。 在 Application 和 Domain 命名空間下信认,我們先將全部類放在一起材义,隨著類的數(shù)量不斷增加,再來考慮必要的分組(我覺得一個文件夾下就放一個類有些矯枉過正嫁赏,所以我寧愿在必要時再進行分組)其掂。
這是我們就要考慮是按照業(yè)務(wù)主題(收據(jù)、交易...)分組還是按照技術(shù)作用(倉庫橄教、服務(wù)清寇、值對象...)分組,但我覺得無論怎樣分組影響都不大护蝶,因為這已經(jīng)是整個代碼組織樹的葉子節(jié)點了华烟,如果需要,在整個組織結(jié)構(gòu)的最底端進行調(diào)整也很簡單持灰,不會影響代碼倉庫的其它部分盔夜。
端口
和 Infrastructure 命名空間一樣,Port 命名空間里核心使用的每一種工具都有一個命名空間,核心通過這些代碼才能使用底層的這些工具喂链。
這些代碼還會被適配器使用返十,它們的作用就是端口和真正工具之間的轉(zhuǎn)換。這種形式簡單得不能再簡單了椭微,端口就是一個接口洞坑,但很多時候它還需要值對象、DTO蝇率、服務(wù)迟杂、構(gòu)建起、查詢對象甚至是倉庫本慕。
共享內(nèi)核
我們把在組件之間共享的代碼放到 Shared Kernel 命名空間下排拷。嘗試了幾種不同的共享內(nèi)核內(nèi)部結(jié)構(gòu)之后,我無法找到一種適用于所有情況的結(jié)構(gòu)锅尘。有些代碼和Core\Component
一樣按組件劃分很合理(例如 Entity ID 顯然屬于一個組件)监氢,有些代碼這樣劃分卻不合適(例如,事件可能被多個組件觸發(fā)或監(jiān)聽)藤违。也許要結(jié)合使用兩種劃分的思路浪腐。
用戶區(qū)里的編程語言擴展
最后,我們還有一些自己對編程語言的擴展纺弊。這個系列中前面一篇文章已經(jīng)討論過牛欢,這些代碼本可以放在編程語言中,卻因為某些原因沒有淆游。比如傍睹,在 PHP 中我們可以想到的是 DateTime 類,它基于 PHP 提供的類擴展犹菱,提供了一些額外的方法拾稳。另一個例子是 UUID 類,盡管 PHP 沒有提供腊脱,但是這個類天然就是純粹的访得、對領(lǐng)域無感,因此可以在任意項目中使用陕凹,并且不依賴任何領(lǐng)域悍抑。
這些代碼用起來和編程語言自己的提供的功能沒啥區(qū)別,因此我們要完全掌控這些代碼杜耙。然而搜骡,這并不是意味著我們不能使用第三方庫。我們能用而且應(yīng)該用佑女,只要合理记靡,但是這些庫應(yīng)該用我們自己的實現(xiàn)包裝起來(這樣的話我們可以方便的切換背后的第三方庫)谈竿,而應(yīng)用代碼應(yīng)該直接使用這些包裝代碼。最終摸吠,這些代碼可以自成項目空凸,使用自己的 CVS 倉庫,被多個項目使用寸痢。
強化架構(gòu)
上述就是所有我們決定要落地的思路和方法呀洲,這需要大量投入,也不容易掌握轿腺。就算我們掌握了所有思路和方法两嘴,但我們終究還是人類,所以我們一定會犯錯族壳,我們的同事也會犯錯,事情就是這樣趣些。
就像我們?yōu)榱吮苊鈱懘a時犯的錯進入生產(chǎn)環(huán)境而編寫測試一樣仿荆,我們也必須對代碼倉庫的結(jié)構(gòu)做點什么。
在 PHP 的世界里坏平,我們有一個叫做 Deptrac 的小工具可以做這種檢查(但我敢打保票其它編程語言也有類似的工具)拢操,這個小工具由 Sensiolabs 創(chuàng)建。我們可以通過一個 yaml 文件進行配置舶替,我們可以在其中配置有哪些層級令境,以及層級之間有哪些依賴。然后我們使用命令行執(zhí)行測試顾瞪,這意味著測試可以輕松地在 CI 中執(zhí)行舔庶,就像我們可以在 CI 種執(zhí)行其它測試一樣。(譯注陈醒,對于 Java 語言來說惕橙,也有類似的工具 https://www.archunit.org/,可以用它把依賴關(guān)系的規(guī)則寫成自動化測試钉跷,但是不能生成依賴圖弥鹦。)。
我們還可以創(chuàng)建依賴圖爷辙,將依賴可視化地展示出來彬坏,包括那些違反實現(xiàn)配置好的規(guī)則集的依賴關(guān)系:
總結(jié)
應(yīng)用遵循某種領(lǐng)域結(jié)構(gòu)組成,也遵循某種技術(shù)結(jié)構(gòu)(即架構(gòu))組成膝晾。這兩種結(jié)構(gòu)才是一個應(yīng)用的與眾不同之處栓始,而不是它使用的工具、庫或者傳達機制玷犹。如果我們想讓一個應(yīng)用可以長時間的維護混滔,這兩種結(jié)構(gòu)都要清晰的體現(xiàn)在代碼倉庫中洒疚,這樣開發(fā)者才能知道、理解坯屿、遵循油湖,并在需要時改進。
這種清晰度讓我們可以在編碼的同時理解邊界领跛,這能反過來幫助我們保持應(yīng)用的模塊化設(shè)計乏德,做到高內(nèi)聚低耦合。
再一次重申吠昭,之前文章里提到的這些思路和實踐大多來自于遠比我優(yōu)秀和經(jīng)驗豐富的開發(fā)者喊括。我和我在不同公司的同事們進行過反復(fù)討論,也在企業(yè)應(yīng)用代碼中進行過嘗試矢棚,在我參與過的項目中都能得到很好地應(yīng)用郑什。
但是,我堅信沒有銀彈蒲肋,沒有均碼的鞋子蘑拯,沒有圣杯。
本文介紹的思路和解耦可以被視為適用于大多是企業(yè)應(yīng)用的通用模板兜粘,如過有必要申窘,不要猶豫,對其進行調(diào)整孔轴。我們總是要對上下文進行評估并竭盡所能剃法,但我希望并相信這個模板是一個不錯的開始,至少值得一試路鹰。
如果你想看看實現(xiàn)了這個模板的 Demo 項目贷洲,我 fork 了 Symfony Demo 應(yīng)用并按照上面的思路進行了重構(gòu)。你可以在這里找到我的重構(gòu)悍引。