# Clean Architecture 實作篇 (第二版)閱讀筆記 英文書名: Get Your Hands Dirty on Clean Architecture 作者: Tom Hombergs ## Ch1 可維護性 甚麼是可維護性? 可維護性與所有面向息息相關。 架構設計、測試、功能實現性等等都與可維護性有關係。 更不用提開發者的心理層面的樂趣。 ## Ch2 階層式架構的問題點 1. 資料庫驅動開發或是以資料結構本身為思考出發點進行設計: 設計的目的是甚麼? 是為了符合業務需求。 業務的行為才是所有事情背後的驅動力。 但是通常階層式架構,在進行設計的時候,我們最常從最底層的資料庫本身開始思考,並圍繞這它進行設計。但如果從業務邏輯的角度來看,這樣根本不合理,應該是從業務邏輯本身作為思考的起點才對! 只有正確實作業務邏輯之後,再去確保其他階層實作才對。 2. 在階層中偷吃步: 上層的物件如果很難訪問怎麼辦? 偷吃步的方式就是將它從上層移動到下層,這樣就容易多了,對吧? 3. 難以測試: 有時候,我們只是想做一個簡單的操作,因此會想透過偷吃步的方式,直接讓網頁層去訪問儲存層,導致以後測試過程中進行模擬的對象,除了領域層以外,還加入了網頁層。 這只會讓測試的難度往上提升。 4. 在現有程式架構中看不出使用案例是甚麼: 由於傳統式階層架構沒有考慮使用案例。某個領域的邏輯基本上散落各個地方。而偷吃步的實作方式,更容易加劇前面的問題。 5. 難以平行分工作業: 階層式架構逼迫你必須要按照儲存層 -> 領域層 -> 網頁層的順序進行開發。實務上根本難以平行作業。 ![image](https://hackmd.io/_uploads/rJo1Ucj-R.png) 圖、傳統式階層架構 ## Ch3 依賴反轉 Dependency Inversion ### 單一職責原則 Single Responsibility Principle 在進入依賴反轉的正題之前,先來討論 SOLID 設計原則中的單一職責原則。 首先,先思考一個問題: **設計上如果違反了這個原則會發生甚麼事情?** 傳統階層式架構的長相可能像這樣 Web -> domain -> persistance 上述架構的問題在於: Web 通常需要使用 domain 的物件,而 domain 通常需要使用 persistance 的物件。因此,當 persistance 的物件需要修改的時候,後面使用到的 domain 層,甚至是 Web 層就有可能需要同步進行修改。這樣如鎖鏈一般相連的相依性關係,當有新的需求需要我們改動設計的時候,往往需要修改超過一個以上的地方。 跟據 SOLID 設計原則,這樣明顯違反了單一職責原則 (以下皆用 SRP 作為代稱)。在本書中,作者 Tom Hombergs 重申了 SRP 背後的代表意義是: > SRP原則真正的定義應該如下所示: > 每個元件應該僅有一種且唯一一種被修改的理由。 這個是每個開發者都會遇到的痛點,如果違反了 SRP 的時候,當你有新需求時,就會有超過一個地方需要修改。而隨著時間演進,系統越發龐大的情況下,需要修改的地方將會越來越多。 ### 依賴反轉原則 Dependency Inversion Principle 什麼是依賴反轉? 在釐清這個概念之前,先搞清楚什麼是依賴(Dependency)? Depend 這個單字的劍橋辭典的英文解釋是 > to be decided by or to change according to the stated thing。 例句 - Whether or not we go to Mexico for our holiday depends on the cost. 簡單來說,A仰賴B,或是借用 [深入淺出依賴反向原則 Dependency Inversion Principle](https://www.jyt0532.com/2020/03/24/dip/) 中所提到的動詞: 換句話說,A**需要**B。 想參考其他說明可以看影片 [Dependency Inversion: What, Why & How? | By Example](https://www.youtube.com/watch?v=-3Z9L6sIAMM) 的說明。 舉一個簡單的例子來說明依賴反轉 一台電動車如果需要前進,它需要車子本身的馬達提供動力。電動車沒有馬達,就不能夠前進。 程式碼範例如下 ```csharp public Class ElectricCar { private Motor _motor; public ElectricCar() { _motor = new Motor(); # ElectricCar 類別需要 Motor 類別 } public void Run() { _motor.Run(); } } ``` 那什麼叫做依賴反轉? 意思是電動車要前進,是不是可以不需要依靠自身的馬達動力,改由外界提供動力,也依然能夠前進? ```csharp public Class ElectricCar { private Motor _motor; public ElectricCar(Motor inputPower) { _motor = inputPower; # Motor 類別改由外部提供 } public void Run() { _motor.Run(); # 系統依然正常運作 } } ``` 原本,`ElectricCar` 類別沒有 `Motor` 類別不行。 不然前進的時候會遇到問題,這個時候 `ElectricCar` 類別與 `Motor` 類別耦合在一起。 但是,在第二個程式範例中,`Motor` 類別轉變成由外部提供,這個時候,變成由其他人確保能夠順利打造出 `Motor` 類別,而不是我們的 `ElectricCar` 類別,代表兩者解除耦合關係了! 通常,打造出 `Motor` 類別的職責,在現代的軟體中會改由 IoC Container 注入相依性的物件。例如,`Autofac`。 高階層模組依賴低階層模組,這樣設計的問題已經在 Ch2 階層式架構的問題點章節點出來了,下面就不再贅述。 而依賴反轉原則便是解決上述問題的解決方案。 依賴反轉原則的想法是,高階層的模組不應該仰賴低階層模組的實作。如果想要打破上傳統階層式架構設計的問題,將依賴關係進行反轉,改由高階層模組依賴抽象層,而不再依賴低階層模組。低階層模組則是專注實作抽象層的方法。 ![image](https://hackmd.io/_uploads/H1T2wK9Z0.png) 圖、傳統階層式架構設計 ![image](https://hackmd.io/_uploads/BkwGaFcbC.png) 圖、依賴反轉後的模組關係 如果從實作層面的角度出發,那麼依賴反轉之後的程式碼樣貌大概會長得像下圖實務上依賴反轉後的樣貌所示。 ![image](https://hackmd.io/_uploads/SyPLtKjWR.png) 圖、實務上依賴反轉後的樣貌 高階層模組內部含有抽象層的方法,我們直接呼叫即可。至於抽象層的實作,則是透過不同方法注入(例如,透過建構子注入、公開方法注入注入,以及 C# 提供的 Property 注入) 高階層模組。 ## Ch4 程式架構 六角形架構最早是由 Alistair Cockburn 在他個人部落格文章 [Hexagonal architecture](https://alistair.cockburn.us/hexagonal-architecture/) 所提出的概念。 ![image](https://hackmd.io/_uploads/ryGVSqLbC.png) 圖、 Hexagonal Architecture (image source: [Hexagonal architecture](https://subscription.packtpub.com/book/programming/9781839211966/2/ch02lvl1sec11/hexagonal-architecture)) 實作的架構以及對應的程式碼可以參考作者本人的教學範例 buckpal,該專案的 github repo [連結](https://github.com/thombergs/buckpal)在此。 下圖 buckpal 是buckpal的專案架構,是一種能夠展現架構的套件結構; 也是本書中推薦的一種整理方式。 ![image](https://hackmd.io/_uploads/BJaM59sbR.png) 圖、buckpal專案架構概覽 ## Ch5 使用案例實作 * 本專案採用 DDD,因此領域相關的資訊,請參考 buckpal 中 application 資料夾中的 domain 資料夾。 * Domain Entity 可以參考 domain 資料夾下的 model 資料夾,例如 Account.java。 * 使用案例可以參考 domain 資料夾下的 service 資料夾,例如 GetAccountBalanceService.java。 ## Ch6 網頁層轉接器實作 * 網頁層轉接器可以參考 adapter 資料夾下的 in/web 資料夾,例如 SendMoneyController.java。 ## Ch7 儲存層轉接器實作 * 儲存層轉接器可以參考 adapter 資料夾下的 out/persistence 資料夾,例如 AccountPersistenceAdapter.java。 ## Ch8 架構測試 8-1 測試金字塔 上層: 系統測試 > 最少 中層: 整合測試 > 次之 底層: 單元測試 > 最多 8-2 領域實體的單元測試 最容易寫單元測試,方法單純 8-3 使用案例的單元測試 使用案例使用領域實體,並改變領域實體的狀態,通常是針對狀態進行測試 (State Verification)。 8-4 網頁層轉接器的整合測試 網頁層轉接器的整合測試會橫跨外部系統(外部的六角形系統)以及領域系統(業務領域的六角形系統),因此通常是進行協同測試(Collaboration Verification)。 8-5 儲存層轉接器的整合測試 同上 8-6 系統主要路徑的系統測試 最終必須要可以不用隔離框架的系統測試,使用具體而微的真實系統進行測試。 畢竟隔離框架不是萬能,必須要由測試撰寫者思考所有可能回傳情境,但終歸沒辦法模擬所有回傳況。 8-7 要多少測試才算夠? 測試覆蓋率沒有標準答案 100 % -> 不代表沒問題,因為可以全部都寫 Happy Path,或是不做任何驗證,也可以達成。 但不代表是好的測試 0 % -> 一定有問題 重點在於: 請思考專案中能幫助我們快速找到問題的單元測試覆蓋率有多高? 8-8 如何讓軟體邁向可維護性的目標 ## Ch9 架構層之間的對應關係 這邊講的對應關係,是在說明跨層之間我們的物件方法,他的輸入以及輸出物件應該如何設計 "看圖才好懂" 9-1 不對應策略 (No Mapping) 完全不設計 優點: 不用煩惱輸入以及輸出物件怎麼設計 缺點: 當我們輸入與輸出物件共用同一個物件Mapping Class,我們很容易因為輸入端或輸出端邏輯改變,因而需要修改 Mapping Class,導致違反 SRP。 ____ ____ | 輸入 | -> | 輸出 | ____ ____ 9-2 雙向對應策略 (Two-Way Mapping) 網頁層如果需要將資料輸入領域層,就需要將網頁層的資料類別傳換成領域層的資料類別; 同理,領域層的資料類別如果要輸出回去網頁層,就需要將領域層的資料類別轉換成網頁層的。 因為對應上有來有回,因此稱作雙向對應。 優點: 1. 不同層之間脫鉤,不會互相影響 2. 概念單純,因為對應關係明確好懂。 缺點: 1. 可能需要維護重複內容的程式碼,但這個代價比起違反 SRP 導致有兩個問題來源而需要修改 Mapping Class來得好。 2. 難除錯,尤其牽涉到泛型 (generic) 以及反射 (Reflection)。 3. 比起全對應策略,還是有一定的耦合 4. 不是萬靈丹, 如果只是 CRUD 而已,這樣做可能會拖慢開發進度 9-3 全對應策略 (Full Mapping) 每種作業都有一個模型。這是以作業(operation)為單位進行設計的應對策略。 優點: 1. 命令 (Command) 的設計要求資訊必須要極度清楚,沒有模糊空間。 2. 好維護 3. 適合 網頁層 <-> 領域層。 缺點: 1. 需要撰寫更多對應的程式碼 (Mapping Code)_ 2. 不適合領域層 <-> 儲存層。因為它也不是萬靈丹, 如果只是 CRUD 而已,這樣做可能會拖慢開發進度 9-4 單向對應策略 (One-Way Mapping) 所有架構層的資料物件都會實作同一個介面(指向同個介面),以對應的getter 方法取得對應的值 優點: 不用做對應的轉譯 隱藏在介面方法後面,不用擔心領域物件狀態被修改 缺點: 實作較難 層之間的資料格式差異過大則無法使用 9-5 如何選擇對應策略? 修改型: 網頁層<->領域層 全部對應策略 + 領域層<->儲存層 不對應策略 查詢型: 網頁層<->領域層 不對應策略 + 領域層<->儲存層 不對應策略 ## Ch10 應用程式組裝 10-1 組裝有甚麼好談的? 如果領域層需要儲存層控制器,就在自己這個物件初始化該物件的話,基本上就會相依於該物件,導致違反依賴反轉原則。 因此,需要有一個獨立的模組(例如,相依注入的套件)來幫助我們解決上述問題。 10-2 透過純程式碼組裝 許多語言都已經有發展出相依注入的套件,例如 C# 的Autofac 就是一個常見的例子。 ## Ch11 理性看待偷吃步 11-1 偷吃步的破窗效應 破窗效應(https://zh.wikipedia.org/zh-tw/%E7%A0%B4%E7%AA%97%E6%95%88%E5%BA%94)維基百科所介紹的 "以一幢有少許破窗的建築為例,如果那些窗沒修理好,可能將會有破壞者破壞更多的窗戶。最終他們甚至會闖入建築內,如果發現無人居住,也許就在那裡佔領、定居或者縱火。" 當開發者認為程式碼雜亂沒有管理,那麼他很有可能認為讓現有的程式碼更亂一點也沒關係。 11-2 第一步的重要性 根據破窗效應,當一個破壞出現之後,其他人很有可能會群起仿效。 因此,才需要在開發的早期及早修補破窗。 如果有偷吃步的地方,也要有工具(架構決策紀錄 Architecture Decision Record, ADR)錄下來,以利時間壓力過了之後進行善後工作。 11-3 在使用案例之間共用模型 因為共用,因此就有兩個修改共用模型的理由,便會需要頻繁地修改共用模型。這就是違反SRP的代價。 11-4 把領域實體當作是輸入或輸出模型 與 11-3 的問題類似,領域實體如果作為輸入模型或輸出模型,那麼就有輸入端與輸出端兩個修改領域實體的理由,一樣容易違反SRP 11-5 省略輸入傳接埠 沒有輸入傳接埠作為防腐層,而直接使用外部的物件的話,根據過往開發的經驗,如果外部傳入的物件某些邏輯修改了,那麼你六角形內部內部的使用案例區塊極為可能發生錯誤。 11-6 省略服務 省略了整個服務層,代表網路層與儲存層共用模型。 共用模型不是不行,但是共用模型的弊病在前面的小節 (11-3 在使用案例之間共用模型) 已經提過,在此不重複敘述。 ## Ch12 強化架構中的邊界 12-1 邊界與依賴關係 當輸入與輸出埠建構完畢之後,這樣會導引使用者用輸入輸出埠所要求的方式進行資料交換。 12-2 存取修飾子 (Access Modifier) 1) 因為有存取修飾子的設計,只公開必要的方法讓外界可以使用,內部的私有方法外界是不行。 2) C# 中, internal 存取修飾子在技術上可以做到只讓 DLL 內部的類別可以呼叫該方法,一旦跨越DLL則不行。 因此也做到了適當的軟體隔離。 12-3 編譯後適應函數 利用軟體工具檢查是否有違反依賴反轉原則。 Java 有 ArchUnit 框架可以使用,C# 在 GitHub 上有類似的框架, 例如 針對 .NET Standard 的 [NetArchTest](https://github.com/BenMorris/NetArchTest) ArchUnit 移植框架 [ArchUnitNET](https://github.com/TNG/ArchUnitNET) 至於循環相依,C# 的依賴注入框架 autofac 有提供檢查方法。 12-4 建製成品 撰寫編譯腳本的時候,其實就是在檢查整個設計有沒有違反依賴反轉原則。 12-5 如何讓軟體邁向可維護性的目標 善用軟體本身的設計與自動化架構檢查框架 ## Ch13 管理多個 Bounded Context 用一張圖就能代表整章的概念。當我們有多個 Bounded Context 的時候,可以利用下圖的概念進行管理。Bounded Context 本身並不需要考慮彼此的溝通問題,除非有其必要性。 ![image](https://hackmd.io/_uploads/rkXvXWFGA.png) 圖片來源: [Managing Multiple Bounded Contexts](https://www.packtpub.com/en-ar/product/get-your-hands-dirty-on-clean-architecture-second-edition-13-9781805128373/chapter/chapter-13-managing-multiple-bounded-contexts) ## Ch14 以元件為基礎的軟體架構方法 14-1 透過元件進行模組化 首先針對元件進行說明。 作者 Tom Hombergs 是這麼形容元件的: > 我更喜歡用"元件"來描述一組精心設計的、用來實作某種有用功能的類別,它可以與其他類別組合在一起,建立複雜的系統。 模組化帶來的大量的好處,首先第一個就是能夠專業分工。例如,NASA 當初在打造阿波羅太空飛行器時,即使許多分工者彼此不互相認識與交流,在模組規格明確的情況下,一樣不影響打造出一流的產品出來。 參考資料: [從登月計劃看電腦軟體發展](https://hackmd.io/@sysprog/apollo-guidance-computer) 14-2 案例研究: 打造一個檢查引擎元件 與 *12-3 編譯後適應函數*小節提到的概念類似。 14-3 強化元件邊界 如果只有軟性的宣導某個作業的流程,未必所有人都會遵守(完美只存在於理想的大同世界)。 以 Java 的 ArchUnit 套件為例子,我們將某個元件的所有類別,都放進名為 internal 的套件中,並且檢查 internal 套件沒有被整個套件以外的類別參考的話。 一旦違反的話,專案將會建置失敗,直接在建置階段就強制將違規的設計擋在門外。 14-4 如何讓軟體邁向可維護性的目標 與 12-5 的結論類似,善用自動化架構檢查框架幫忙檢查。 ## Ch15 選擇你的架構風格 根據不同情境,選擇適合自己的架構。 如果只是簡單的 CRUD,那就不必擔心架構問題,但往往事情都沒有這麼簡單。 如果開始往領域的方向發展的話,那個本書所倡導的六角型架構將會越來越重要。