人人都是架構師-清晰架構 | 京東物流技術團隊

發表於2024-02-28

前言

瞭解清晰架構之前需要大家先熟悉以下常見架構方案:

EBI架構(Entity-Boundary-Interactor Architecture)
領域驅動設計(Domain-Driven Design)
埠與介面卡架構(Ports & Adapters Architecture,又稱為六邊形架構)
洋蔥架構(Onion Architecture)
整潔架構(Clean Architecture)
事件驅動架構(Event-Driven Architecture)
命令查詢職責分離模式(CQRS,即Command Query Responsibility Segregation)
面向服務的架構(Service Oriented Architecture)

清晰架構(Explicit Architecture,直譯為顯式架構)是將上述架構的部分優勢整合之後產生的另一種架構,因其2017年已經出現,已經不算是一種新的架構,實際應用的專案尚且較少。以下主要介紹架構的形成及各步驟的意義。

1 架構演化過程

1.1 系統的基本構建塊

埠和介面卡架構明確地識別出了一個系統中的三個基本程式碼構建塊:

  • 執行使用者介面所需的構建塊;
  • 系統的業務邏輯,或者應用核心;
  • 基礎設施程式碼。

1.2 工具

在遠離【應用核心】的地方,有一些應用會用到的工具,例如資料庫引擎、搜尋引擎、Web 伺服器或者命令列控制檯(雖然最後兩種工具也是傳達機制)。

把命令列控制檯和資料庫引擎都是應用使用的工具,關鍵的區別在於,命令列控制檯和 Web 伺服器告訴我們的應用它要做什麼,而資料庫引擎是由我們的應用來告訴它做什麼。

1.3 將傳達機制和工具連線到應用核心

連線工具和應用核心的程式碼單元被稱為介面卡(源自埠和介面卡架構)。介面卡有效地實現了讓業務邏輯和特定工具之間可以相互通訊的程式碼。
“告知我們的應用應該做什麼”的介面卡被稱為主介面卡或主動介面卡,而那些“由我們的應用告知它該做什麼”的介面卡被稱為從介面卡或者被動介面卡。

1.3.1 埠

介面卡需要按照應用核心某個特定的入口的要求來建立,即埠。在大多數語言裡最簡單的形式就是介面,但實際上也可能由多個介面和 DTO 組成。
埠(介面)位於業務邏輯內部,而介面卡位於其外部,這一點要特別注意。要讓這種模式按照設想發揮作用,埠按照應用核心的需要來設計而不是簡單地套用工具的 API。

1.3.2 主介面卡或主動介面卡

主介面卡或主動介面卡包裝埠並透過它告知應用核心應該做什麼。它們將來自傳達機制的資訊轉換成對應用核心的方法呼叫。

換句話說,我們的主動介面卡就是 Controller 或者控制檯命令,它們需要的介面(埠)由其他類實現,這些類的物件透過構造方法注入到 Controller 或者控制檯命令。
再舉一個更具體的例子,埠就是 Controller 需要的 Service 介面或者 Repository 介面。Service、Repository 或 Query 的具體實現被注入到 Controller 供 Controller 使用。
此外,埠還可以是命令匯流排介面或者查詢匯流排介面。這種情況下,命令匯流排或者查詢匯流排的具體實現將被注入到 Controller 中, Controller 將建立命令或查詢並傳遞給相應的匯流排。

1.3.3 從介面卡或被動介面卡

和主動介面卡包裝埠不同,被動介面卡實現一個埠(介面)並被注入到需要這個埠的應用核心裡。

舉個例子,假設有一個需要儲存資料的簡單應用。我們建立了一個符合應用要求的持久化介面,這個介面有一個儲存資料陣列的方法和一個根據 ID 從表中刪除一行的方法。介面建立好之後,無論何時應用需要儲存或刪除資料,都應該使用實現了這個持久化介面的物件,而這個物件是透過構造方法注入的。
現在我們建立了一個專門針對 MySQL 實現了該介面的介面卡。它擁有儲存陣列和刪除表中一行資料的方法,然後在需要使用持久化介面的地方注入它。
如果未來我們決定更換資料庫供應商,比如換成 PostgreSQL 或者 MongoDB,我們只用建立一個專門針對 PostgreSQL 實現了該介面的介面卡,在注入時用新介面卡代替舊介面卡。

1.3.4 控制反轉

這種模式有一個特徵,介面卡依賴特定的工具和特定的埠(它需要提供介面的特定實現)。但業務邏輯只依賴按照它的需求設計的埠(介面),它並不依賴特定的介面卡或工具。
換句話說,介面卡根據使用的工具不同可以靈活變更,但是業務邏輯產生的介面基本不會變化。

這意味著依賴的方向是由外向內的,這就是架構層面的控制反轉原則。
再一次強調,埠按照應用核心的需要來設計而不是簡單地套用工具的 API。

1.4 應用核心的結構

洋蔥架構採用了 DDD 的分層,將這些分層融合進了埠和介面卡架構。這種分層為位於埠和介面卡架構“六邊形”內的業務邏輯帶來一種結構組織,和埠與介面卡架構一樣,依賴的方向也是由外向內。

1.4.1 應用層

在應用中,由一個或多個使用者介面觸發的應用核心中的過程就是用例。例如,在一個 CMS 系統中,我們可以提供普通使用者使用的應用 UI、CMS 管理員使用的獨立的 UI、命令列 UI 以及 Web API。這些 UI(應用)可以觸發的用例可能是專門為它設計的,也可以是多個 UI 複用的。
用例定義在應用層中,這是 DDD 提供的第一個被洋蔥架構使用的層。

這個層包括了應用服務(以及它們的介面),也包括了埠與介面卡架構中的介面,例如 ORM 介面、搜尋引擎介面、訊息介面等等。如果我們使用了命令匯流排和查詢匯流排,命令和查詢分別對應的處理程式也屬於這一層。

1.4.2 領域層

繼續向內一層就是領域層。這一層中的物件包含了資料和運算元據的邏輯,它們只和領域本身有關,獨立於呼叫這些邏輯的業務過程。它們完全獨立,對應用層完全無感知。

1.領域服務
我們偶爾會碰到某種涉及不同實體的領域邏輯,當然,無論實體是否相同,直覺告訴我們這種領域邏輯並不屬於這些實體,這種邏輯不是這些實體的直接責任。
所以,我們的第一反應也許是把這些邏輯放到實體外的應用服務中,這意味著這些領域邏輯就不能被其它的用例複用:領域邏輯應該遠離應用層。
解決方法是建立領域服務,它的作用是接收一組實體並對它們執行某種業務邏輯。領域服務屬於領域層,因此它並不瞭解應用層中的類,比如應用服務或者 Repository。另一方面,它可以使用其他領域服務,當然還可以使用領域模型物件。

2.領域模型
在架構的正中心,是完全不依賴外部任何層次的領域模型。它包含了那些表示領域中某個概念的業務物件。這些物件的例子首先就是實體,還有值物件、列舉以及其它領域模型中用到的任何物件。
領域事件也“活在”領域模型中。當一組特定的資料發生變化時就會觸發這些事件,而這些事件會攜帶這些變化的資訊。換句話說,當實體變化時,就會觸發一個領域事件,它攜帶著發生變化的屬性的新值。這些事件可以完美地應用於事件溯源。

1.5 元件

目前為止,我們都是使用層次來劃分程式碼,但這是細粒度的程式碼隔離。根據 Robert C. Martin 在尖叫架構中表達的觀點,按照子域和限界上下文對程式碼進行劃分這種粗粒度的程式碼隔離同樣重要。這通常被叫做“按特性分包”或者“按元件分包”,和“按層次分包”相呼應。

我是“按元件分包”方式的堅定擁護者,在此我厚著臉皮將 Simon Brown 按元件分包的示意圖做了如下修改:

這些程式碼塊在前面描述的分層基礎上再進行了“橫切”,它們是應用的元件(譯)。
元件的例子包括認證、授權、賬單、使用者、評論或帳號,而它們總是都和領域相關。像認證和授權這樣的限界上下文應該被看作外部工具,我們應該為它們建立介面卡,把它們隱藏在某個埠之後。

1.5.1 元件解耦

與細粒度的程式碼單元(類、介面、特質、混合等等)一樣,粗粒度的程式碼單元(元件)也會從高內聚低耦合中受益。
我們使用依賴注入(透過將依賴注入類而不是在類內部初始化依賴)以及依賴倒置(讓類依賴抽象,即介面和抽象類,而不是具體類)來解耦類。這意味著類不用知道它要使用的具體類的任何資訊,不用引用所依賴的類的完全限定類名。
以同樣的方式完全解耦元件意味著元件不會直接瞭解其它任何元件的資訊。換句話說,它不會引用任何來自其它元件的細粒度的程式碼單元,甚至都不會引用介面!這意味著依賴注入和依賴倒置對元件解耦是不夠用的,我們還需要一些架構層級的結構。我們需要事件、共享核心、最終一致性甚至發現服務!

1.觸發其它元件的邏輯
當一個元件(元件 A)中有事情發生需要另一個元件(元件B)做些什麼時,我們不能簡單地從元件 A 直接呼叫元件 B 中的類/方法,因為這樣 A 就和 B 耦合在一起了。
但是我們可以讓 A 使用事件派發器,派發一個領域事件,這個事件將會投遞給任何監聽它的元件,例如 B,然後 B 的事件監聽器會觸發期望的操作。這意味著元件 A 將依賴事件派發器,但和 B 解耦了。
然而,如果事件本身“活在” A 中,這將意味著 B 知道了 A 的存在,就和 A 存在耦合。要去掉這個依賴,我們可以建立一個包含應用核心功能的庫,由所有元件共享,這就是共享核心。這意味著兩個元件都依賴共享核心,而它們之間卻沒有耦合。共享核心包含了應用事件和領域事件這樣的功能,而且還包含規格物件,以及其它任何有理由共享的東西。記住共享核心的範圍應該儘可能的小,因為它的任何變化都會影響所有應用元件。而且,如果我們的系統是語言異構的,比如使用不同語言編寫的微服務生態,共享核心需要做到與語言無關的,這樣它才能被所有元件理解,無論它們是用哪種語言編寫的。例如,共享核心應該包含像 JSON 這樣無關語言的事件描述(例如,名稱、屬性,也許還有方法,儘管它們對規格物件來說更有意義)而不是事件類,這樣所有元件或者微服務都可以解析它,還可以自動生成各自的具體實現。

這種方法既適用於單體應用,也適用於像微服務生態系統這樣的分散式應用。然而,這種方法只適用於事件非同步投遞的情況,在需要即時完成觸發其它元件邏輯的上下文中並不適用!元件 A 將需要向元件 B 發起直接的呼叫,例如HTTP。這種情況下,要解耦元件,我們需要一個發現服務,A 可以詢問它得知請求應該傳送到哪裡才能觸發期望的操作,又或是向發現服務發起請求並由發現服務將請求代理給相關服務並最終返回響應給請求方。這種方法會把元件和發現服務耦合在一起,但會讓元件之間解耦。例如jsf。

2.從其它元件獲得資料
原則上,元件不允許修改不“屬於”它的資料,但可以查詢和使用任何資料。
1)元件之間共享資料儲存
當一個元件需要使用屬於其它元件的資料時,比如說賬單元件需要使用屬於賬戶元件的客戶名字,賬單元件會包含一個查詢物件,可以在資料儲存中查詢該資料。簡單的說就是賬單元件知道任何資料集,但它只能透過查詢只讀地使用不“屬於”它的資料。
2)按元件隔離的資料儲存
這種情況下,這種模式同樣有效,但資料儲存層面的複雜度更高。
元件擁有各自的資料儲存意味著每個資料儲存都包含:

  • 一組屬於它的資料,並且只允許它自己修改這些資料,讓它成為單一事實來源;
  • 一組其它元件資料的副本,它自己不能修改這些資料,但元件的功能需要這些資料,而且一旦資料在其所屬的元件中發生了變化,這些副本需要更新。

每個元件都會建立其所需的其它元件資料的本地副本,在必要時使用。當資料在其所屬的元件中發生了變化,該元件將觸發一個攜帶資料變更的領域事件。擁有這些資料副本的元件將監聽這個領域事件並相應地更新它們的本地副本。

1.6 控制流

如前所述,控制流顯然從使用者出發,進入應用核心,抵達基礎設施工具,再返回應用核心並最終返回給使用者。但這些類到底是是如何配合的?哪些類依賴哪些類?我們怎樣把它們組合在一起?

1.6.1 沒有命令/查詢匯流排

如果沒有命令匯流排,控制器要麼依賴應用服務,要麼依賴查詢物件。

上圖中我們使用了應用服務介面,儘管我們會質疑這並沒有必要。因為應用服務是我們應用程式碼的一部分,而且我們不會想用另外一種實現來替換它,儘管我們可能會徹底地重構它。

1.6.2 有命令/查詢匯流排

如果我們的應用使用了命令/查詢匯流排,UML 圖基本沒有變化,唯一的區別是控制器現在會依賴匯流排、命令或查詢。它將例項化命令或查詢,將它們傳遞給匯流排。匯流排會找到合適的處理程式接收並處理命令。
在下圖中,命令處理程式接下來將使用應用服務。然而,這不總是必須的,實際上大多數情況下,處理程式將包含用例的所有邏輯。只有在其它處理程式需要重用同樣的邏輯時,我們才需要把處理程式中的邏輯提取出來放到單獨的應用服務中。

匯流排和命令查詢,以及處理程式之間沒有依賴。這是因為實際上它們之間應該互相無感知,才能提供足夠的解耦。只有透過配置才能設定匯流排可以發現哪些命令,或者查詢應該由哪個處理程式處理。
如你所見,兩種情況下,所有跨越應用核心邊界的箭頭——依賴——都指向內部。如前所述,這是埠和介面卡架構、洋蔥架構以及整潔架構的基本規則。

1.7 共享核心

共享核心由 DDD 之父 Eric Evans 定義,它是多個限界上下文之間共享的程式碼,由開發團隊決定:

[…] 兩個團隊同意共享的領域模型的子集。當然,和模型子集一起共享還包括程式碼的子集,還有和這部分模型有關的資料庫設計。這部分明確要共享的內容有著特殊的狀態,而且在沒有和其他團隊達成一致的情況下不應該修改。
Shared Kernel(http://ddd.fed.wiki.org/view/shared-kernel), Ward Cunningham 的 DDD wiki

所以基本上,它可能是任何型別的程式碼:領域層程式碼、應用層程式碼、庫,隨便什麼程式碼。

然而,在這份心智地圖裡,我們將它當做一些特定型別的程式碼的子集。共享核心包含的是領域層和應用層的程式碼,這些程式碼會在限界上下文之間共享,讓這些上下文可以互相通訊。
這意味著,例如,一個或多個限界上下文觸發的事件可以在其它的限界上下文裡被監聽到。需要和這些事件一起共享的還有它們用到的所有資料型別,例如:實體 ID、值物件、列舉,等等。事件不應該直接使用像實體這樣的複雜物件,因為將它們序列化到佇列中或是從佇列中反序列化時都會遇到一些問題,所以共享的程式碼不應該太寬泛。
當然,如果我們手中的是一個由不同語言開發的微服務組成的多語言系統,共享核心必須是描述性的語言,格式是 json、xml、yaml 或者其它,這樣所有的微服務都能理解。
因此,共享核心就完全和其餘的程式碼以及元件完全解耦了。這樣很好,因為這意味著儘管元件耦合了共享核心,但元件之間不再耦合。共享程式碼可以被清晰地識別出來,並輕鬆地提取到一個獨立的庫中。
如果我們決定將一個限界上下文從單體中分離出來並提取成一個微服務,這也會很方便。我對共享程式碼瞭然於心,可以輕鬆地將共享核心提取到一個庫中。而這個庫即可以安裝到單體中,也可以安裝到微服務中。

2 用程式碼體現架構

2.1 兩張腦圖

第一張腦圖由一系列同心圓層級組成,它們最終按照業務維度的應用模組切分,形成元件。在這張圖裡,依賴的方向由外向內,意味著內層對外層可見,而外層對內層不可見。

第二張則是一組平面的層級,其中最上面的一層就是前面這張同心圓,下一層是元件之間共享的程式碼(共享核心),再下一層使是我們自己對程式語言的擴充套件,最下面一層則是實際使用的程式語言。這裡的依賴方向是自上而下的。

2.2 體現架構的程式碼風格

使用體現架構的程式碼風格,意味著程式碼風格(編碼規範、類/方法/變數命名約定、程式碼結構…)某種程度上可以和閱讀程式碼的人交流領域和架構的設計意圖。要實現體現架構的程式碼風格,主要有兩種思路。

“[…] 體現架構的程式碼風格能讓你給程式碼的閱讀者留下提示,幫助他們正確地推斷出設計意圖。”
—George Fairbanks(https://links.jianshu.com/go?to=https%3A%2F%2Fresources.sei.cmu.edu%2Fasset\_files%2FPresentation%2F2013\_017\_001\_48651.pdf)

第一種思路是透過程式碼製品的名字(類、變數、模組…)來傳達領域和架構的含義。因此,如果一個類是處理收據(Invoice)實體的倉庫(Repository),我們就應該將它命名成InvoiceRepository,從這個名字我們就可以看出,它處理的是收據領域的概念,而它在架構中被當做一個倉庫。這可以幫助我們理解它應該放在哪個地方,何時使用它以及如何使用它。但是,我認為程式碼倉庫中並不是每個程式碼製品都需要這樣做,例如,我覺得不必為每個實體(Entity)都加上字尾Entity,這樣做就有些畫蛇添足,徒增噪音。

“[…] 程式碼應該體現架構。換句話說,我一看到程式碼,就應該能夠清晰地區分出各種元件[…]”
—Simon Brown(https://links.jianshu.com/go?to=http%3A%2F%2Fwww.codingthearchitecture.com%2F2014%2F06%2F01%2Fan\_architecturally\_evident\_coding\_style.html)

第二種思路是讓程式碼倉庫中的頂級製品明確地區分出各個子域,即領域維度的模組,也就是元件。
第一種思路應該很清楚,無需贅述。但第二種思路有點兒微妙,我們得深入探討一下。

2.3 讓架構清晰的展現出來

在我的第一張圖裡,我們已經看到,在最粗粒度的層級上,我們只有三種不同用途的程式碼:

  • 使用者介面,這裡的程式碼就是為了適配某個用例的傳達機制;
  • 應用核心,這裡的程式碼就是用例和領域邏輯;
  • 基礎設施,這裡的程式碼就是為了適配應用核心所需的工具/庫。

因此,在原始碼的根目錄下我們可以建立三個資料夾來體現這三類程式碼,一個資料夾對應一個類別的程式碼。這三個資料夾表示三個名稱空間,稍後我們甚至可以建立測試來斷言核心對使用者介面和基礎設施可見,反過來卻不可見,也就是說,我們可以測試由外向內的依賴方向。

2.3.1 使用者介面

一個 Web 企業應用通常擁有多套 API,例如,一套給客戶端使用的 REST API,還有一套給第三方應用使用的 web-hook, 業務還有一套需要維護的遺留 SOAP API,或者還有一套給全新移動應用使用的 GraphQL API…
這樣的應該通常還有一些 CLI 命令,用於定時作業(Cron Job)或按需的維護操作。
當然,還有普通使用者可以使用的網站本身,但也許還有另一個供應用管理員使用的網站。
這些全都是同一個應用的不同檢視,全都是同一個應用的不同使用者介面。
實際上我們的應用可能擁有多個使用者介面,其中有些還是供非人類使用者(第三方應用)使用的。我們透過檔案/名稱空間來區分並隔離這些使用者介面,來展現出這一點。
使用者介面主要有三類:API、CLI 和網站。所以我們在UserInterface根名稱空間裡為每個類別建立一個資料夾,將不同介面的型別清晰地區分開來。

下一步,如果有必要的話,我們還可以繼續深入每種型別的名稱空間,再建立更細分類的使用者介面的名稱空間(CLI 可能不需要再細分了)。

2.3.2 基礎設施

和使用者介面一樣,我們的應用使用了多種工具(庫和第三方應用),例如 ORM、訊息佇列、SMS 提供商。
此外,上述每一種工具都可以有不同的實現。例如,考慮一家公司業務擴張到另一個國家的情況,由於價格的因素,不同的國家最好採用不同的 SMS 提供商:我們需要埠相同的介面卡的不同實現,這樣使用時可以互相替換。另一個例子是對資料庫 Schema 進行重構或者切換資料庫引擎,需要(或決定要)切換 ORM 時:我們會在應用中注入兩種 ORM 介面卡。

因此,在Infrastructure名稱空間來說,我們先給每一種工具型別建立一個名稱空間(ORM、MessageQueue、SmsClient),然後再每一種工具型別內部為每一種用到的供應商(Doctrine、Propel、MessageBird、Twilio…)的介面卡在建立一個名稱空間。

2.3.3 核心

在Core名稱空間下,可以按照最粗粒度的層級劃分出三類程式碼: 元件(Component)、共享核心(Shared Kernel) 和 埠(Port)。為這三個類別建立資料夾/名稱空間。

1.元件
在 Component 名稱空間下,我們為每個元件創一個名稱空間,然後在每個元件名稱空間下,我們再分別為應用(Application)層和領域(Domain)層分別建立一個名稱空間。 在 Application 和 Domain 名稱空間下,我們先將全部類放在一起,隨著類的數量不斷增加,再來考慮必要的分組(我覺得一個資料夾下就放一個類有些矯枉過正,所以我寧願在必要時再進行分組)。
這是我們就要考慮是按照業務主題(收據、交易…)分組還是按照技術作用(倉庫、服務、值物件…)分組,但我覺得無論怎樣分組影響都不大,因為這已經是整個程式碼組織樹的葉子節點了,如果需要,在整個組織結構的最底端進行調整也很簡單,不會影響程式碼倉庫的其它部分。

2.埠
和 Infrastructure 名稱空間一樣,Port 名稱空間裡核心使用的每一種工具都有一個名稱空間,核心透過這些程式碼才能使用底層的這些工具。
這些程式碼還會被介面卡使用,它們的作用就是埠和真正工具之間的轉換。這種形式簡單得不能再簡單了,埠就是一個介面,但很多時候它還需要值物件、DTO、服務、構建起、查詢物件甚至是倉庫。

3.共享核心
我們把在元件之間共享的程式碼放到 Shared Kernel 名稱空間下。嘗試了幾種不同的共享核心內部結構之後,我無法找到一種適用於所有情況的結構。有些程式碼和Core\Component一樣按元件劃分很合理(例如 Entity ID 顯然屬於一個元件),有些程式碼這樣劃分卻不合適(例如,事件可能被多個元件觸發或監聽)。也許要結合使用兩種劃分的思路。

2.3.4 使用者區裡的程式語言擴充套件

最後,我們還有一些自己對程式語言的擴充套件。這個系列中前面一篇文章已經討論過,這些程式碼本可以放在程式語言中,卻因為某些原因沒有。比如,在 PHP 中我們可以想到的是 DateTime 類,它基於 PHP 提供的類擴充套件,提供了一些額外的方法。另一個例子是 UUID 類,儘管 PHP 沒有提供,但是這個類天然就是純粹的、對領域無感,因此可以在任意專案中使用,並且不依賴任何領域。

這些程式碼用起來和程式語言自己的提供的功能沒啥區別,因此我們要完全掌控這些程式碼。然而,這並不是意味著我們不能使用第三方庫。我們能用而且應該用,只要合理,但是這些庫應該用我們自己的實現包裝起來(這樣的話我們可以方便的切換背後的第三方庫),而應用程式碼應該直接使用這些包裝程式碼。最終,這些程式碼可以自成專案,使用自己的 CVS 倉庫,被多個專案使用。

3 透過文件描述架構

我們有哪些可供選擇的文件工具來表達整個應用的構建塊以及應用如何工作?!

UML
4+1 架構檢視模型
架構決策記錄
C4 模型
依賴圖
應用地圖

3.1 C4 模型

C4 模型是 Simon Brown 發明的,是我目前看到的關於軟體架構文件的最好思路。我會快速地用自己的語言來闡述主要的思路,但使用的還是他的圖例。
其思路是用四種不同粒度(或者“縮放”)層級來記錄軟體的架構:
第一級:系統上下文圖
第二級:容器圖
第三級:元件圖
第四級:程式碼圖

3.1.1 第一級:系統上下文圖

這是最粗粒度的圖。它的細節很少但其主要目標是描述應用所處的上下文。因此,這幅圖中只有一個方塊代表整個應用,其它圍繞著應用的方塊代表了應用要進行交付的外部系統和使用者。

3.1.2 第二級:容器圖

現在,我們將應用放大,也就是上一級圖中的藍色方塊,在這一級它對應的是下圖中的虛線框。
在這個粒度級別,我們將看到應用得容器,一個容器就是一個應用中技術上獨立的一小部分,例如一個移動 App,一個 API 或者一個資料庫。它還描述了應用使用的主要技術和容器之間的通訊方式。

3.1.3 第三級:元件圖

元件圖展示的是一個容器內的元件。在 C4 模型上下文裡,每個元件就是應用的一個模組,不光是領域維度的模組(如賬單、使用者…)還包括純粹的功能模組(如 email、sms…)。因此這個層級的圖向我們展示了一個容器的主要齒輪和齒輪之間的齧合關係。

3.1.4 第四級:程式碼圖

這是最細粒度的圖,目的是描述一個元件內部的程式碼結構。在這個層級,我們使用的是表示類級別製品的 UML 圖。

4 總結

清晰架構集百家之長,天然有很多優勢:

  • 從外向內,越向內越偏核心原則,核心原則相對穩定。核心原則就是常規的領域層,提供核心能力
  • 外層基於核心原則適配不同的業務場景,組裝內層的能力。這裡的外層就是常規的介面層到應用層,主要使用主動介面卡模式,重點關注 BFF(BackendsForFrontends) 及對內層能力的聚合
  • 內層不依賴外層,不受業務變化而變化。關注能力的擴充套件,完成核心策略實現
  • 邊界明顯,尤其是領域層與應用層之間
  • CQRS 機制,耦合度低,透過外層組裝內層能力動態適配業務變化,擴充套件性高

這只是一份指南!應用才是你的疆域,現實情況和具體用例才是運用這些知識的地方,它們才能勾勒出實際架構的輪廓!
我們需要理解所有這些模式,但我們還時常需要思考和理解我們的應用需要什麼,我們應該在追求解耦和內聚的道路上走多遠。這個決定可能受到許多因素的影響,包括專案的功能需求,也包括構建應用的時間期限,應用壽命,開發團隊的體驗等等因素。
應用遵循某種領域結構組成,也遵循某種技術結構(即架構)組成。這兩種結構才是一個應用的與眾不同之處,而不是它使用的工具、庫或者傳達機制。如果我們想讓一個應用可以長時間的維護,這兩種結構都要清晰的體現在程式碼倉庫中,這樣開發者才能知道、理解、遵循,並在需要時改進。
這種清晰度讓我們可以在編碼的同時理解邊界,這能反過來幫助我們保持應用的模組化設計,做到高內聚低耦合。

附錄:

  1. 軟體架構編年史(譯):https://www.jianshu.com/p/b477b2cc6cfa
  2. The Software Architecture Chronicles:https://herbertograca.com/2017/07/03/the-software-architecture-chronicles/
  3. 技術案例—基於 DDD 思想的技術架構戰略調整:https://www.6aiq.com/article/1648170246451
  4. 中文圖


作者:京東物流 李國樑

來源:京東雲開發者社群 自猿其說 Tech 轉載請註明來源

相關文章