原文:Clean architecture for the rest of us
作者:Suragch
這是一篇介紹性文章。
如果你是一名高階軟體工程師,可以到此為止了,這篇文章不適合你。這篇文章是寫給那些像我一樣的普通程式設計師的,他們寫著亂七八糟的程式碼,建立著像義大利麵一樣混亂的架構,但卻對構建乾淨的、可維護的、適應性強的東西很著迷。
前言
我通常不購買計算機相關的書籍,因為它們實在是過時的太快了。還有就是,反正這些書中資訊在網上都可以查得到。但在一年前,我閱讀了 Robert Martin 編寫的 Clean Code,這本書真實的幫我改善了開發軟體的方式。所以,當我看到同一作者另外一本書《Clean Architecture》出版的時候,便毫不猶豫的買下了它。
和 Clean Code 一樣,Clean Architecture 中講述了大量經的起時間考驗的原則,這些原則適用於任何人編寫的任何程式碼。如果你在網上搜尋書名,會發現有些人並不認同作者的觀點。顯然,我不是要在這裡批判他。我只是知道 Robert Martin (又名 Uncle Bob)已經從事程式設計超過50年,而我沒有。
儘管這本書理解起來有點困難,但我會盡最大努力,把書中的重要概念加以總結和解釋,讓普通人也能夠理解。作為一名軟體架構師,我仍然在不斷學習成長,所以,請以批判的眼光閱讀我寫的東西。
什麼是 clean architecture?
架構 (Architecture) 是指專案的整體設計,是程式碼放入類(classes)、檔案(files)、元件(components)、模組(modules) 的組織結構,以及這些程式碼單元之間相互關聯的方式。架構定義了應用程式在哪裡執行核心功能,以及這些功能如何與資料庫、使用者介面等事物進行互動。
Clean 架構是一種易於理解、可以隨著專案發展靈活改變的專案組織方式。這不是隨隨便便就可以辦到的,需要有意識的規劃。
Clean architecture 的特點
構建一個易於維護的大型專案的祕訣是:將類或檔案拆分到不同的元件中,每個元件都可以獨立於其他元件進行修改。讓我們用幾張圖片來說明這一點:
上面的圖片中,如果想用刀代替剪刀,你需要做什麼?你必須解開連線到筆、墨水瓶、膠帶和指南針的繩子。然後你再將這些物品重新系到刀上。也許這對於刀來說是可以了。但如果鋼筆和膠帶說:“等等,我們需要剪刀。” 所以現在筆和膠帶不能正常工作了,不得不在做改變。這個改變反過來又會影響那些連線到它們上面的相關物體。 一團糟。
對比下圖:
現在我們應該如何替換剪刀呢?我們只需要從便利貼下面拉出剪刀的繩子,然後把系在刀子上的新繩子插進去。容易多了。便利貼不在乎,因為繩子甚至沒有綁在它上面。
第二張圖所代表的架構顯然更容易改變。只要便利貼不需要被經常的更換,這個系統就很容易維護。同樣的,這種架構可以使軟體更易於維護和變更。
內圈是應用程式的領域層 (domain layer) ,用來放置業務規則。我們所說的“業務”不一定是指公司,它只是意味著你的應用程式的本質,即程式碼的核心功能。例如:翻譯應用程式的核心功能是翻譯,線上商店的本質是要出售商品。這些業務規則通常相當的穩定,因為你不太可能經常改變應用的本質。
外層是基礎設施層 (infrastructure layer),包括 UI、資料庫、網路 API、以及軟體框架之類的東西。相對於領域,這些更容易發生變化。舉個例子,你更有可能更改 UI 按鈕的外觀,而不是更改貸款的計算方式。
領域和基礎設施之間設定了一個明顯的邊界,目的是讓領域對基礎設施毫無感知。這意味著 UI 和資料庫依賴業務規則,但業務規則不依賴 UI 和資料庫。這使它成為一個外掛架構 (plugin architecture),無論 UI 是一個網頁、桌面應用程式、還是移動應用都沒有關係;資料是儲存在 SQL、NoSQL 還是雲端也沒有關係,領域根本就不關心。這使得更改基礎設施變得十分容易。
定義術語
上圖中的兩個圈層可以進一步細化。
在這裡,領域層被細分成 entities 和 use cases,adapter layer 形成了領域和基礎設施層之間的邊界。這些術語可能有點令人困惑。讓我們分別來看一下。
Entities(實體)
一個 entity 是一組對應用程式功能至關重要的相關的業務規則。在物件導向的程式語言中,一個 entity 的所有規則會以成員方法的形式組合到一個類中。即使沒有應用,這些規則仍然存在。例如,銀行可能有個規則,對貸款收取 10% 的利息,無論是在紙上計算還是使用計算機,這個利息都是 10%。這是書中一個 entity 類的示例 (191頁):
Entities 對其他層一無所知,它們不依賴任何東西。也就是說,它們不使用外層中的任何類或元件。
Use cases(用例)
Use cases 是特定應用程式的業務規則,描述如何使系統自動化執行。這決定了應用程式的行為。下面是書中關於 use cases 業務規則的一個示例 (192頁,稍作了些修改):
Gather Info for New Loan
Input: Name, Address, Birthdate, etc.
Output: Same info + credit score
Rules:
1. Validate name
2. Validate address, etc.
3. Get credit score
4. If credit score < 500 activate Denial
5. Else create Customer (entity) and activate Loan Estimation
Use cases 與 entities 互動,並依賴 entities,但它對更外層一無所知。它們不在乎是網頁還是 iPhone 應用程式,也不關心資料是儲存在雲端還是本地的 SQLite 資料庫。
該層定義介面或者抽象類,以供外層使用。
Adapters(介面卡)
Adapters,也稱為 interface adapters,是領域和基礎設施之間溝通的翻譯員 (translators)。例如,它們從 GUI 獲取輸入資料,並將其重新打包成便於 use cases 和 entities 使用的形式。然後它們從 use cases 和 entities 獲取輸出,並將其重新打包成便於 GUI 顯示或資料庫儲存的形式。
Infrastructure(基礎設施)
這層是所有 I/O 元件所在的地方:UI、資料庫、框架、裝置等。這是最不穩的的一層。由於這一層中的事物很可能發生變化,因此它們儘可能遠離更穩定的領域層。因為是相互分離的,所以對其進行修改或元件替換都相對容易。
實現 clean architecture 的原則
因為下面的一些原則有著令人迷惑的名字,所以我在上面的解釋中特意沒有使用它們。但是,要想實現我所描述的架構設計,必須遵循這些原則。如果這部分讓你頭暈目眩,可以直接跳到文章最後的 最終總結 部分。
下面的前五個原則通常縮寫為 SOLID,以方便記憶。它們是類級別的原則,但具有適用於元件 (相關類的集合)的類似對應物。元件級別的原則遵循 SOLID 原則。
單一職責原則 (Single Responsibility Principle - SRP)
這是 SOLID 中的 S。SRP 說的是一個類應該只有一個職責。類可能有多個方法,但這些方法一起協同工作來完成一件主要事情。對類的修改應該只有一個原因。舉個例子,如果財務辦公室提了一個需求需要修改這個類,同時人力資源部也有一個需求需要以不同的方式修改這個類,這時,修改這個類就存在了兩個原因。那麼,這個類應該被拆分為兩個獨立的類,以保證每個類都只有一個原因去修改。
開閉原則 (Open Closed Principle - OCP)
這是 SOLID 中的 O。Open 意味著對於擴充套件是開放的。Close 意味著對於修改是關閉的。因此,你應該有能力向類或元件新增功能,同時,又不需要修改現有功能。要怎麼做的呢?首先需要保證每個類或元件只有一個單一職責,然後將相對穩定的類隱藏在介面的後面。這樣,當不太穩定的類不得不修改的時候不會影響到相對穩定的類。
里氏替換原則 (Liskov Substitution Principle - LSP)
這是 SOLID 中的 L。我猜是需要 L 來拼成 SOLID,但“替換”才是你需要記住的。該原則意味著較低層級的類或元件可以被替換,但不能影響到較高層級的類或元件的行為。可以通過抽象類或介面來實現該原則。例如,在 Java 中,ArrayList 和 LinkedList 都實現了 List 介面,它們可以相互替換。如果這個原則應用到架構級別,MySQL 可以被 MongoDB 替換,同時不影響領域層的邏輯。
介面隔離原則 (Interface Segregation Principle - ISP)
這是 SOLID 中的 I。ISP 指的是使用介面將一個類和使用它的類分開,介面只暴露依賴類所需要的方法子集。這樣,不在介面暴露的方法子集中的其他方法發生修改時,不會影響到依賴類。
依賴倒置原則 (Dependency Inversion Principle - DIP)
這是 SOLID 中的 D。這意味著相對不穩定的類和元件應該依賴於相對穩定的類和元件,而不是反過來。如果一個穩定的類依賴一個不穩定的類,那麼每次不穩定的類發生變化,將會影響到穩定的類,所以需要翻轉依賴的方向。要怎麼做呢?通過使用抽象類,或者把穩定的類隱藏在介面的後面。
所以,像下面這樣一個穩定的類使用易變的類的情況:
class StableClass {
void myMethod(VolatileClass param) {
param.doSomething();
}
}
應該建立一個介面,並讓易變的類實現這個介面:
class StableClass {
interface StableClassInterface {
void doSomething();
}
void myMethod(StableClassInterface param) {
param.doSomething();
}
}
class VolatileClass implements StableClass.StableClassInterface {
@Override
public void doSomething() {
}
}
這樣就翻轉了依賴方向。易變得類知道穩定的類的類名,但穩定的類對易變的類一無所知。
使用抽象工廠模式 (Abstract Factory partten)是實現此目的的另一種方法。
重用/釋出等效原則 (Reuse/Release Equivalence Principle - REP)
REP 是一個元件級別的原則。重用 (Reuse) 是指一組可重用的類或模組。釋出 (Release) 是指以版本號釋出。這個原則是說,你釋出的任何東西都應該可以作為內聚的單元進行重複使用,而不應該是不相干的類的隨機集合。
共同閉合原則 (Common Closure Principle -CCP)
CCP 是一個元件級別的原則。它說的是元件應該是一些類的集合,這些類在相同的時間由於同樣的原因被修改。如果對這些類的修改基於不同的原因,或者修改的頻率不一致,那麼這個元件應該被拆分。該原則與上面提到的單一職責原則 (Single Responsibility Principle - SRP)基本相同。
通用複用原則 (Common Reuse Principle - CRP)
CRP 是一個元件級別的原則。它說的是不應該依賴那些包含你不需要的類的元件。這些元件應該被拆分到使用者不必依賴那些他不使用的類的程度。該原則與上面提到的介面隔離原則 (Interface Segregation Principle - ISP)基本相同。
這三個原則 (REP, CCP, and CRP) 相互矛盾。過多的拆分或過多的分組都會導致問題。需要根據實際情況平衡這些原則。
非迴圈依賴原則 (Acyclic Dependency Principle - ADP)
ADP 意味著在專案中不應該出現依賴迴圈。例如,如果元件 A 依賴元件 B,元件 B 依賴元件 C,而元件 C 又依賴元件 A,那麼就存在一個依賴迴圈。
在嘗試對系統進行更改時,這樣的迴圈會產生重大問題。打破依賴迴圈的一種解決方案,是使用依賴倒置原則 (Dependency Inversion Principle - DIP)在元件之間新增一個介面。如果不同的個人或團隊對不同的元件負責,那麼這些元件應該以自己的版本號單獨釋出。這樣,一個元件的更改不會立即影響到其他團隊。
穩定依賴原則 (Stable Dependency Principle - SDP)
這個原則說的是,依賴關係應該建立在穩定的方向上。也就是說,較不穩定的元件應該依賴較穩定的元件。這最大限度的降低了變更帶來的影響。一些元件本身就是容易發生變化的,這沒有關係,我們要做的是不要讓穩定的元件依賴它們。
穩定抽象原則 (Stable Abstraction Principle - SAP)
SAP 說的是:一個元件越穩定,它就應該越抽象,也就是它應該包含的抽象類越多。抽象類更容易擴充套件,因此這可以防止穩定的元件變得過於僵化。
最終總結
以上內容總結了《Clean Architecture》一書的主要原則,但我還想補充一些其他的要點。
測試
建立一個外掛架構的好處是使程式碼更具可測試性。當專案中有很多依賴的時候,程式碼是很難測試的。但當你擁有一個外掛框架,測試會變得容易很多,僅僅需要你用 Mock 物件替換一個資料庫依賴項(或者其他的任何元件)。
我總是在測試 UI 的時候感到很糟糕。我做了一個遍歷 GUI 的測試,但一旦我對 UI 做了更改,測試就中斷了,最終我只能刪除這個測試。我意識到我應該在介面卡層 (adapter layer)建立一個 Presenter 物件。Presenter 獲取業務規則的輸出,並根據 UI 檢視的需要格式化所獲得的所有內容。UI 檢視物件除了顯示 Presenter 提供的預格式化資料之外什麼都不做。這樣修改程式碼之後,就可以獨立於 UI 測試 Presenter 的程式碼了。
建立一個特殊的測試 API 來測試業務規則。它應該與介面介面卡分離,以便在應用程式結構發生變化時測試不會中斷。
根據用例 (use cases) 劃分元件
我在上面談到了領域和基礎設施層。如果將這些看作是水平方向的層級,則可以根據應用程式的不同用例 (user cases),將它們在垂直方向上劃分為不同的元件組。就像是一個分層蛋糕,每個切片都是一個用例 (use cases),切片中的每一層構成一個元件。
例如,在視訊網站上,一個用例 (use case) 是觀眾 (viewer) 觀看視訊。所以有一個 ViewerUseCase 元件、一個 ViewerPresenter 元件、一個 ViewerView 元件,等等。另一個用例 (use case) 是針對上傳視訊到網站的釋出者 (publisher)。對於他們,應該有一個 PublisherUseCase 元件、一個 PublisherPresenter 元件、一個 PublisherView 元件,等等。還有一個用例 (use case) 可能是針對站點的管理員。以這種方式,通過對水平層進行垂直方向的切片來建立單個元件。
部署應用程式的時候,可以以最有意義的任何方式對元件進行分組。
強制分層
你可能擁有世界上最好的架構,但如果新來的開發人員新增了一個繞過邊界的依賴項,這將完全違背了架構設計的初衷。防止這種情況發生的最佳方法是:使用編譯器來保護架構。例如,在Java中,可以將類打包為 private,以便在那些不應該知道它們的模組面前隱藏起來。另一種選擇是使用第三方軟體,它可以幫助你檢查是否有東西在使用它不應該使用的東西。
只在需要時增加複雜性
不要從一開始就過度設計你的系統,只有在需要的時候才使用更多的架構。但是在體系結構中維護一些邊界,會使元件在未來更容易爆發。舉個例子:首選,你可能會部署一個外部的單體應用程式,但在內部,類保持著適當的邊界。稍後,你可能將它們分解為單獨的模組。在後來,你可以將它們部署為服務。只要沿著保持分層和邊界的路走,你就可以自由調整它們的部署方式。通過這種方式,你不會創造可能永遠也用不到的不必要的複雜性。
細節
在開始一個專案時,應該首先處理業務規則,其他的都是細枝末節。資料庫是一個細節,UI 是一個細節,作業系統是一個細節,Web API 是一個細節,框架也是一個細節。對這些細節的決定應該儘可能的延後。這樣,當你需要它們的時候,你將站在一個絕佳的位置幫助你作出明智的選擇。這對你初始的開發工作沒有影響,因為領域層對基礎設施層一無所知。當你準備好選擇資料庫時,填寫資料庫介面卡程式碼然後將其插入到架構中。當你準備好 UI 時,填寫 UI 介面卡程式碼,然後將其插入到架構中。
最後一點建議
- 不要把 Entity 物件用作在外層傳遞的資料結構,應該使用獨立的資料模型物件。
- 專案的頂級組織架構應該清楚地告訴人們這個專案是關於什麼的。這叫做 screaming architecture。
- 走出去,開始將這些課程付諸實踐。只有使用這些原則,你才能真正學會它們。
練習:製作依賴圖
開啟你當前的一個專案,並在一張紙上畫出依賴關係圖。為專案中的每一個元件或類畫一個方框,然後遍歷每個類,看看這些類的依賴。任何命名的類都是依賴項。從正在檢查的類的方框畫一個箭頭指向命名的類或元件的方框。
當你遍歷完所有的類,請考慮下面的問題:
- 業務規則在哪裡 (entities and use cases) ?
- 業務規則是否依賴其他東西?
- 如果你不得不使用不同的資料庫、UI 平臺、或程式碼框架,有多少個類或元件將受到影響?
- 是否有依賴迴圈?
- 為了建立外掛架構,您需要進行哪些重構?
結論
《Clean Architecture》這本書的精髓是你需要建立一個外掛架構 (plugin architecture)。出於相同的原因在同一時間需要同時修改的類應該組合在一起成為元件。業務規則元件是相對更加穩定的,它們應該對相對易變的基礎設施元件一無所知,這些基礎設施元件處理 UI、資料庫、網路、程式碼框架和其他的細節功能。元件層級之間的邊界,是通過介面介面卡來維護的。這些介面介面卡在層級之間傳輸資料,並沿著指向更穩定的內部元件的方向保持依賴關係。
我學到了很多東西。我希望你也是。如果我在哪裡歪曲了這本書,請告知我。您可以在我的 GitHub 個人資料 中找到我的聯絡資訊。
進一步學習
我盡我最大的努力全面的總結了 Clean Architecture,但是你會在書中找到更多資訊。值得花時間讀一下這本書。事實上,我推薦閱讀 Robet Martin 寫的以下這三本書。我給出了這些書在亞馬遜上的連結,但如果你購買二手副本,你可能會發現它們更便宜。 我按照推薦閱讀的順序列出了它們。 這些書都不會很快的過時。