2021年1月14日 星期四

Clean Architecture 入門

最近有機會重新架構一個新的專案,大家討論之後覺得想要試試 Clean Architecture 的架構,就稍微花了點時間學習了一下。不過因為只有粗略地翻過其中的一些章節,因此還只有在比較高層次概觀的理解,對於部份細節的實作還不是相當熟悉。這裡稍微紀錄一下目前為止理解的東西。

    穩定的元件與不穩定的元件

    在 Clean Architecture 的概念中,它首先將系統拆分為不同的元件,並把元件分類為「穩定的元件」與「不穩定的元件」兩種。

    「穩定的元件」代表的是不易變動的元件;而「不穩定的元件」則代表的是易變動的元件。這裡的「變動」意義跟 Design Pattern 裡談的是同樣的概念,在探討一個元件是否容易因為需求或環境或各種原因而需要去改寫它。一個穩定的元件,代表的是它一旦寫完後,就不太可能產生變動,在 Clean Architecture 的看法中,商業邏輯就是屬於穩定的元件,因為商業邏輯直接關係到這個系統會被看作是什麼系統,所以不太容易產生變動。反之,不穩定的元件就是很可能會產生變動的地方,例如資料庫、API 介面等等的東西,都很容易因為各種原因而變化。

    有了「穩定」和「不穩定」的定義後,Clean Architecture 帶出的觀點是,元件與元件之間的依賴關係,應該要總是由不穩定的元件指向穩定的元件,如下圖。其中依賴關係指的是,從 Java 的程式碼來說,當 A 類別的 import 清單裡有 B 類別時,我們就可以畫出 A → B 的依賴關係,表示 A 類別依賴 B 類別。


    節錄自 [1]

    那麼為什麼應該要由穩定的元件指向不穩定的元件呢?其實稍微思考一下就可以很快理解到,這是很正常的期待。因為我們定義不穩定的元件是時常變更的元件,因此如果穩定的元件依賴不穩定的元件時,就表示不穩定的元件的變更非常容易會連帶影響到穩定的元件。比如說 A → B 的依賴關係是 A 在某個地方呼叫了 B.process() 函式,但 B 是個不穩定元件,它的函式邏輯甚至是函式的參數都常常因應需求在變動,此時 A → B 的依賴關係,就會很容易導致每當 B 變動時 A 也跟著需要做相應的變動。這會造成 A 在概念上也變成 B 的一部分,同時也使得 A 看起來就像是個不穩定元件了。

    架構的核心是什麼?

    當我們在設計系統架構時,我們會思考,什麼才是這個架構最重要的東西呢?什麼才是架構的核心?核心代表了這個架構、這個系統,當它變更了,某種程度上表示這個系統就變得不是這個系統了。Clean Architecture 告訴我們一個辨識核心的方法:screaming architecture。我們希望當任何人看到這個架構時,都會一眼直接看穿這個架構。我們可能會說「這是個人事系統」、「這是個會計系統」,但我們不會想要得到「這是個 Spring Boot 系統」、「這是個 PostgreSQL 系統」這樣的答案。

    所以,架構的核心不是使用了什麼框架、也不是使用了什麼資料庫,而是商業邏輯是什麼、使用情境是什麼。例如一個人事系統,會有一段商業邏輯是「當有員工在平日加班兩小時,則他會有兩小時的加班費是以 1.33 倍計算」、「當有員工年資滿一年,則他可以享有 7 天的特休假」。這些商業邏輯綜合起來,表示了這個系統是什麼。

    Clean Architecture 的結構


    節錄自 [1]

    Clean Architecture 的這張同心圓圖很熱門,只要談到 Clean Architecture 幾乎就會出現這張圖,不為什麼,就是因為這張圖就是 Clean Architecture 的主要概念。Clean Architecture 提出一個準則:

    Source code dependencies must point only inward, toward higher-level policies.

    在系統被切分為 Entities、Use Cases、Controllers/Presenters/Gateways 以及 Web/UI/DB/Devices/External Interfaces 這四大元件時,我們同時要注意程式碼的依賴關係必須由外往內指

    這裡我們可以稍微分解一下,圖中最內層的圓圈是 Entities 和 Use Cases,這兩個東西其實就代表了整個系統的商業邏輯、以及這些商業邏輯互動過程所使用的物件。往外一圈是 Controllers/Presenters/Gateways,顯示了當我們要從 Controller 連接 Use Case(商業邏輯)時,我們必須讓依賴的方向是 Controller → Use Case。接著再更往外一圈,會看到~咦?DB 在最外圈。回想最開始我們提到的,依賴關係應該從不穩定的元件指向穩定的元件,在這張圖上即可以表達出這個概念,Entities 和 Use Cases 這兩個代表了整個系統的商業邏輯,應該要是穩定元件,所以應該放在整個圖的最內側。而像是 Controllers 或者甚至是 DB 等可能會依據需求或環境變更的東西,則是不穩定元件,被發配在外面。

    元件的依賴關係與依賴反轉

    看到這裡,也許會察覺到依賴的方向有點奇怪?一般我們在做簡單的 Web 系統時,程式碼的呼叫關係可能會長成這樣:

    Controller → Service → Database

    代表 Controller 收到客戶端的要求後,會呼叫 Service 處理,Service 做了某些事後,最終會去存取 Database 取得必要資訊,然後再回覆給 Controller,Controller 把 Service 回覆的東西透過 Presenter 調整成回覆所需的格式並回覆,完成整個呼叫流程。然而,這樣的依賴關係就違反了 Clean Architecture 的原則了,按照原則,我們的依賴關係應該要長成這樣:

    Controller → Service ← Database

    因為在同心圓的圖中,Controller 和 Database 都位在外層,身為內層的 Service 是不允許存取外層的元件的,Service 跟 Database 的依賴關係反向了,這該怎麼辦呢?這時,依賴反轉(Dependency Inversion)就是時候出場了。我們幫 Database 建立一層介面,讓 Service 存取 Database 的介面,並且讓 Database 的實作類別去實做那個介面,就可以達成將 Service → Database 的依賴關係反轉為 Service ← Database。於是,我們得以讓 Controller 和 Database 都跟 Use Cases 解耦,也就是 Use Cases 不會呼叫 Controller 也不會呼叫 Database,所有的依賴關係都存在於同心圓的外層,也就是 Controller 與 Database 等不穩定的元件上,使得不穩定元件可以很容易地替換,卻不會影響到核心的 Use Cases。

    參考資料
    1. Clean Architecture: A Craftsman's Guide to Software Structure and Design
    2. mattia-battiston/clean-architecture-example
    3. 架构整洁之道
    4. 整潔的架構

    沒有留言: