人人都是架構師-清晰架構

来源:https://www.cnblogs.com/Jcloud/archive/2023/07/12/17546755.html
-Advertisement-
Play Games

清晰架構是將領域驅動、整潔架構等架構的部分優勢整合之後產生的另一種架構,因其2017年已經出現,已經不算是一種新的架構,實際應用的項目尚且較少。以下主要介紹架構的形成及各步驟的意義 ...


前言

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

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ## Grid佈局 ### 1 概述 網格佈局(Grid)將王爺分成一個個網格,可以任意組合不同的網格,做出各種各樣的佈局。Grid佈局與Flex不具有一定的相似性,都可以指定容器內部多個項目的位置,但是他們存在重大的區別。flex佈局時軸線佈局,只能指定項目針對軸線的位置,可以看作是一維佈局;gr ...
  • # flex佈局 ## 上節複習 選擇器進階: 偽類選擇器: 當滿足特定條件時,激活對應的樣式 元素:hover{} 當滑鼠經過元素時,激活樣式 偽元素選擇器: 創建一個虛假的元素.不能被選中.不存在網頁dom中(安全性/性能) 元素::before{content:'內容'} 在元素前面添加內容 ...
  • 一、數據類型: 1、基本數據類型:String、Number、Boolean、Null、Undefined、Symbol 、BigInt 2、引用數據類型:Object、Array、Function、Date、RegExp 二、檢測數據類型的四種方法 1.typeof檢測 特點:typeof只能檢測 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 追憶Scoped 偶然想起了一次面試,二面整體都聊完了,該做的演算法題都做出來了,該背的八股文也背的差不多了,面試官頻頻點頭,似乎對我的基礎和項目經驗都很是滿意。嗯,我內心os本次面試應該十拿九穩了。 突然,面試官說:「我的主技術棧是Rea ...
  • 雖然第三方表格插件多不勝數,但是很多場景還是需要用到原生<table>,掌握html原生table的實現方法,是前端開發的必備技能。例如:print-js列印、html2canvas生成圖片等,用原生table可以規避很多問題。 首先,在寫原生<table>之前,我們先認識一下 border-col ...
  • 1.頁面結構 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> </head> <body> </body> </html> 編碼:charset=“gbk” ;gbk2312,utf-8 2.常見標簽 2.1 ...
  • ## 引言 軟體開發過程中,我們經常會遇到各種設計問題,例如如何管理對象之間的關係、如何優化代碼的可復用性和可維護性等等。為瞭解決這些問題,我們可以使用設計模式。 設計模式是一種被廣泛接受的軟體設計思想,它提供了一套通用的解決方案,可以幫助我們更好地解決常見的軟體設計問題。設計模式是從實踐中總結出來 ...
  • 本文詳細介紹了搭建系統工程架構時需要關註的幾個重要方面。基於產品的價值,做出決策。並從系統工程架構的演進、技術方案的選型、系統規範共識的達成等方面入手,對實施過程中的常見問題給出瞭解決思路。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...