原文:https://herbertograca.com/2019/06/05/reflecting-architecture-and-domain-in-code/
這篇文章是軟件架構編年史(譯)的一部分,這部編年史由一系列關于軟件架構的文章組成。在這一系列文章中,我將寫下我對軟件架構的學習和思考,以及我是如何運用這些知識的。如果你閱讀了這個系列中之前的文章,本篇文章的的內容將更有意義。
在創建應用的時候,讓它可以工作易如反掌。要讓它在處理大量數據的情況下仍然保持性能,會有點困難。但是最難的挑戰是構建一個真正可以維系多年(十年、二十年甚至一百年)的應用程序。
我工作過的大多數公司都有每三到五年就重建應用的歷史,有時甚至不到兩年就要重建。這種做法成本極高,它將極大地影響應用程序的成功,進而極大地影響公司的成功,還會讓開發人員在亂成一鍋粥的代碼庫種凌亂,讓他們想萌生辭職的想法。任何一家志向遠大的正經公司,都無法承受任何經濟上、時間上、聲譽上、客戶上、人才上的損失。
讓應用程序保持可維護性的基石是讓代碼能夠反映出架構和領域,這對防止所有棘手問題至關重要。
清晰架構是對比我經驗更豐富的開發者所倡導的原則以及實踐的合理解釋,也是對我如何組織代碼庫使之反映出項目架構與領域并便于溝通的經驗總結。
在我的上一篇博客(譯文)里,我將這些理念匯總起來并用信息圖和 UML 圖呈現,試圖建立我思考的概念圖譜。
然而,我們怎樣才能把實踐落實到代碼中呢?
在這篇博客里,我將說明我是如何在代碼中體現一個項目的結構和領域的,還將提出一個通用的結構,我認為它能幫助我們規劃好可維護性。
我的兩張腦圖
在這個系列的前兩篇文章里我介紹了兩張腦圖,我用它們來思考代碼和組織代碼倉庫,至少在我腦海里是這樣想的。
第一張腦圖由一系列同心圓層級組成,它們最終按照業務維度的應用模塊切分,形成組件。在這張圖里,依賴的方向由外向內,意味著內層對外層可見,而外層對內層不可見。
第二張則是一組平面的層級,其中最上面的一層就是前面這張同心圓,下一層是組件之間共享的代碼(共享內核),再下一層使是我們自己對編程語言的擴展,最下面一層則是實際使用的編程語言。這里的依賴方向是自上而下的。
體現架構的代碼風格
使用體現架構的代碼風格,意味著代碼風格(編碼規范、類/方法/變量命名約定、代碼結構...)某種程度上可以和閱讀代碼的人交流領域和架構的設計意圖。要實現體現架構的代碼風格,主要有兩種思路。
“[…] 體現架構的代碼風格能讓你給代碼的閱讀者留下提示,幫助他們正確地推斷出設計意圖。”
—George Fairbanks
第一種思路是通過代碼制品的名字(類、變量、模塊...)來傳達領域和架構的含義。因此,如果一個類是處理收據(Invoice)實體的倉庫(Repository),我們就應該將它命名成InvoiceRepository
,從這個名字我們就可以看出,它處理的是收據領域的概念,而它在架構中被當做一個倉庫。這可以幫助我們理解它應該放在哪個地方,何時使用它以及如何使用它。但是,我認為代碼倉庫中并不是每個代碼制品都需要這樣做,例如,我覺得不必為每個實體(Entity)都加上后綴Entity
,這樣做就有些畫蛇添足,徒增噪音。
“[…] 代碼應該體現架構。換句話說,我一看到代碼,就應該能夠清晰地區分出各種組件[…]”
—Simon Brown
第二種思路是讓代碼倉庫中的頂級制品明確地區分出各個子域,即領域維度的模塊,也就是組件。
第一種思路應該很清楚,無需贅述。但第二種思路有點兒微妙,我們得深入探討一下。
讓架構清晰的展現出來
在我的第一張圖里,我們已經看到,在最粗粒度的層級上,我們只有三種不同用途的代碼:
- 用戶界面,這里的代碼就是為了適配某個用例的傳達機制;
- 應用核心,這里的代碼就是用例和領域邏輯;
- 基礎設施,這里的代碼就是為了適配應用核心所需的工具/庫。
因此,在源代碼的根目錄下我們可以創建三個文件夾來體現這三類代碼,一個文件夾對應一個類別的代碼。這三個文件夾表示三個命名空間,稍后我們甚至可以創建測試來斷言核心對用戶界面和基礎設施可見,反過來卻不可見,也就是說,我們可以測試由外向內的依賴方向。
用戶界面
一個 Web 企業應用通常擁有多套 API,例如,一套給客戶端使用的 REST API,還有一套給第三方應用使用的 web-hook, 業務還有一套需要維護的遺留 SOAP API,或者還有一套給全新移動應用使用的 GraphQL API…
這樣的應該通常還有一些 CLI 命令,用于定時作業(Cron Job)或按需的維護操作。
當然,還有普通用戶可以使用的網站本身,但也許還有另一個供應用管理員使用的網站。
這些全都是同一個應用的不同視圖,全都是同一個應用的不同用戶界面。
實際上我們的應用可能擁有多個用戶界面,其中有些還是供非人類用戶(第三方應用)使用的。我們通過文件/命名空間來區分并隔離這些用戶界面,來展現出這一點。
用戶界面主要有三類:API、CLI 和網站。所以我們在UserInterface根命名空間里為每個類別創建一個文件夾,將不同界面的類型清晰地區分開來。
下一步,如果有必要的話,我們還可以繼續深入每種類型的命名空間,再創建更細分類的用戶界面的命名空間(CLI 可能不需要再細分了)。
基礎設施
和用戶界面一樣,我們的應用使用了多種工具(庫和第三方應用),例如 ORM、消息隊列、SMS 提供商。
此外,上述每一種工具都可以有不同的實現。例如,考慮一家公司業務擴張到另一個國家的情況,由于價格的因素,不同的國家最好采用不同的 SMS 提供商:我們需要端口相同的適配器的不同實現,這樣使用時可以互相替換。另一個例子是對數據庫 Schema 進行重構或者切換數據庫引擎,需要(或決定要)切換 ORM 時:我們會在應用中注入兩種 ORM 適配器。
因此,在Infrastructure命名空間來說,我們先給每一種工具類型創建一個命名空間(ORM、MessageQueue、SmsClient),然后再每一種工具類型內部為每一種用到的供應商(Doctrine、Propel、MessageBird、Twilio...)的適配器在創建一個命名空間。
核心
在Core命名空間下,可以按照最粗粒度的層級劃分出三類代碼: 組件(Component)、共享內核(Shared Kernel) 和 端口(Port)。為這三個類別創建文件夾/命名空間。
組件
在 Component 命名空間下,我們為每個組件創一個命名空間,然后在每個組件命名空間下,我們再分別為應用(Application)層和領域(Domain)層分別創建一個命名空間。 在 Application 和 Domain 命名空間下,我們先將全部類放在一起,隨著類的數量不斷增加,再來考慮必要的分組(我覺得一個文件夾下就放一個類有些矯枉過正,所以我寧愿在必要時再進行分組)。
這是我們就要考慮是按照業務主題(收據、交易...)分組還是按照技術作用(倉庫、服務、值對象...)分組,但我覺得無論怎樣分組影響都不大,因為這已經是整個代碼組織樹的葉子節點了,如果需要,在整個組織結構的最底端進行調整也很簡單,不會影響代碼倉庫的其它部分。
端口
和 Infrastructure 命名空間一樣,Port 命名空間里核心使用的每一種工具都有一個命名空間,核心通過這些代碼才能使用底層的這些工具。
這些代碼還會被適配器使用,它們的作用就是端口和真正工具之間的轉換。這種形式簡單得不能再簡單了,端口就是一個接口,但很多時候它還需要值對象、DTO、服務、構建起、查詢對象甚至是倉庫。
共享內核
我們把在組件之間共享的代碼放到 Shared Kernel 命名空間下。嘗試了幾種不同的共享內核內部結構之后,我無法找到一種適用于所有情況的結構。有些代碼和Core\Component
一樣按組件劃分很合理(例如 Entity ID 顯然屬于一個組件),有些代碼這樣劃分卻不合適(例如,事件可能被多個組件觸發或監聽)。也許要結合使用兩種劃分的思路。
用戶區里的編程語言擴展
最后,我們還有一些自己對編程語言的擴展。這個系列中前面一篇文章已經討論過,這些代碼本可以放在編程語言中,卻因為某些原因沒有。比如,在 PHP 中我們可以想到的是 DateTime 類,它基于 PHP 提供的類擴展,提供了一些額外的方法。另一個例子是 UUID 類,盡管 PHP 沒有提供,但是這個類天然就是純粹的、對領域無感,因此可以在任意項目中使用,并且不依賴任何領域。
這些代碼用起來和編程語言自己的提供的功能沒啥區別,因此我們要完全掌控這些代碼。然而,這并不是意味著我們不能使用第三方庫。我們能用而且應該用,只要合理,但是這些庫應該用我們自己的實現包裝起來(這樣的話我們可以方便的切換背后的第三方庫),而應用代碼應該直接使用這些包裝代碼。最終,這些代碼可以自成項目,使用自己的 CVS 倉庫,被多個項目使用。
強化架構
上述就是所有我們決定要落地的思路和方法,這需要大量投入,也不容易掌握。就算我們掌握了所有思路和方法,但我們終究還是人類,所以我們一定會犯錯,我們的同事也會犯錯,事情就是這樣。
就像我們為了避免寫代碼時犯的錯進入生產環境而編寫測試一樣,我們也必須對代碼倉庫的結構做點什么。
在 PHP 的世界里,我們有一個叫做 Deptrac 的小工具可以做這種檢查(但我敢打保票其它編程語言也有類似的工具),這個小工具由 Sensiolabs 創建。我們可以通過一個 yaml 文件進行配置,我們可以在其中配置有哪些層級,以及層級之間有哪些依賴。然后我們使用命令行執行測試,這意味著測試可以輕松地在 CI 中執行,就像我們可以在 CI 種執行其它測試一樣。(譯注,對于 Java 語言來說,也有類似的工具 https://www.archunit.org/,可以用它把依賴關系的規則寫成自動化測試,但是不能生成依賴圖。)。
我們還可以創建依賴圖,將依賴可視化地展示出來,包括那些違反實現配置好的規則集的依賴關系:
總結
應用遵循某種領域結構組成,也遵循某種技術結構(即架構)組成。這兩種結構才是一個應用的與眾不同之處,而不是它使用的工具、庫或者傳達機制。如果我們想讓一個應用可以長時間的維護,這兩種結構都要清晰的體現在代碼倉庫中,這樣開發者才能知道、理解、遵循,并在需要時改進。
這種清晰度讓我們可以在編碼的同時理解邊界,這能反過來幫助我們保持應用的模塊化設計,做到高內聚低耦合。
再一次重申,之前文章里提到的這些思路和實踐大多來自于遠比我優秀和經驗豐富的開發者。我和我在不同公司的同事們進行過反復討論,也在企業應用代碼中進行過嘗試,在我參與過的項目中都能得到很好地應用。
但是,我堅信沒有銀彈,沒有均碼的鞋子,沒有圣杯。
本文介紹的思路和解耦可以被視為適用于大多是企業應用的通用模板,如過有必要,不要猶豫,對其進行調整。我們總是要對上下文進行評估并竭盡所能,但我希望并相信這個模板是一個不錯的開始,至少值得一試。
如果你想看看實現了這個模板的 Demo 項目,我 fork 了 Symfony Demo 應用并按照上面的思路進行了重構。你可以在這里找到我的重構。