在本系列的第一部分,我們介紹了我們在尋找可行架構的道路上所犯過的錯誤。在這部分,我們將介紹傳說中的 Clean Architecture。
當你在谷歌搜索 "clean architecture" 時,你看到的第一張圖片是:
它也被稱為洋蔥架構,因為圖看起來象個洋蔥(你會意識到你需要寫樣板代碼寫到哭);或者是端口和適配器,因為你可以看到右圖的一些端口。六角架構是另一個相似的架構。
Clean Architecture 是前面提到的 Uncle Bob 的心血結晶,他是 《代碼整潔之道》的作者。這種方法的要點是,業務邏輯(也稱為 domain),是宇宙的核心。
掌控你的領域(domain)
當你打開項目時,你應該已經知道這個 app 是做什么的,與技術無關。其它一切都是實現細節。譬如,持久化就是一個細節。定義接口,創建一個快速的粗糙的內存內(in-memory)實現,不要想太多,直到完成業務。然后你可以決定怎樣真正地持久化數據。數據庫,網絡,兩者結合,文件系統 —— 或者仍然保留在內存中,或者結果你根本不需要持久化。總之一句話:內層包含業務邏輯,外層包含實現細節。
話說回來, Clean Architectue 有一些特性使這成為可能:
- 依賴規則
- 抽象
- 層與層之間的通信
I.依賴規則
依賴規則可以用下圖解釋:
外層應該依賴內層。那三個在紅色框框內的箭頭表示依賴。與其使用“依賴”,也許使用“看見”、“知道”、“了解”這類術語更好。在這些術語中,外層看見,知道,了解內層,但內層看不見,也不知道,更不了解外層。正如我們先前所說,內存包含業務邏輯,外層包含實現細節。遵循依賴規則,業務邏輯既看不到,也不知道,更不了解實現細節。這正是我們努力想要做到的。
如何實現依賴規則取決于你。你可以把它們放到不同的包,但小心“內層的”包不要使用“外層的”包。然而,如果有人不知道依賴規則,沒有什么可以阻止他破壞規則。一個更好的方法是把層分離到不同的 Android 模塊(modules,即子項目),并在構建文件(build.grale)中調整依賴,這樣內層就無法依賴外層。
還有值得一提的是,雖然沒人可以阻止你跨層依賴,譬如藍色的層的組件使用紅色的層的組件,但我強烈建議你只訪問相鄰的層的組件。
II.抽象
抽象原則之前已有所暗示。也就是說,當你朝圖中間移動時,東西變得更抽象。 這是有道理的:正如我們所說內層包含業務邏輯,而外層包含實現細節。
甚至可以在多個層之間劃分相同的邏輯組件,如圖所示。 內層定義更抽象的部分,外層定義更具體的部分。
舉個例子說清楚些。我們可以定義一個 “Notifications” 的抽象接口,并將其放到內層,這樣你的業務邏輯需要時可以使用它來向用戶顯示通知。另一方面,我們可以這樣來實現該接口,即使用 Android NotificationManager 顯示通知來實現,并把該實現放到外層。
以這種方式,業務邏輯可以使用這樣的功能 —— 通知(在我們的例子中)—— 但它不了解實現細節:實際的通知是如何實現的。此外,業務邏輯甚至不知道實現細節的存在。來看下面這張圖片:
當將抽象規則和依賴規則組合在一起時,結果是使用通知的抽象業務邏輯既不會看到,也不會知道,更不會了解使用 Android NotificationManager 的具體實現。這很好,因為我們可以在業務邏輯毫不知情的情況下切換具體實現。
讓我們把這種規則組合和標準的三層架構簡單對比下,看看它們各自的抽象和依賴是怎樣的以及如何工作的。
在圖中,你可以看到,標準三層架構的所有依賴最終都傳到數據庫。也就是說,抽象和依賴并不匹配。在邏輯上,業務層應該是 app 的中心,但它卻不是,因為依賴朝向數據庫。
業務層不應該知道數據庫,應該反過來。在 Clean Architecture 中,依賴朝向業務層(內層),并且抽象也提升到業務層,因此它們很好地匹配。
這是重要的,因為抽象是理論,依賴是實踐。抽象是 app 的邏輯布局,依賴關系是(組件)如何實際組合在一起。在 Clean Architecture 中,這兩者是匹配的。而在標準三層架構中則不然,如果你不小心,很容易導致各種邏輯上的不一致和混亂。
III.層與層之間的通信
現在我們將 app 分模塊,將所有內容分開,將業務邏輯放在我們 app 的中心,并在外層實現細節,一切看起來都很棒。 但是你可能很快遇到一個有趣的問題。
如果你的 UI 是一個實現細節,網絡是一個實現細節,業務邏輯在中間,那么我們如何從互聯網獲取數據,經過業務邏輯,然后發送到界面?
業務邏輯在中間,應該協調網絡和界面,但它甚至不知道兩者的存在。這是一個關于通信和數據流的問題。
我們希望數據能夠從外層流向內層,反之亦然,但依賴規則不允許。 讓我們舉個最簡單的例子。
我們只有兩層,綠色和紅色的。綠色的是外層,它知道紅色的,紅色的是內層,它只知道自己。我們希望數據從綠色流向紅色,然后折回綠色。該解決方案先前已經暗示過了,看下圖:
圖的右邊部分顯示了數據流。數據源于 Controller,經過 UseCase(或者替換成你選擇的組件)的輸入端口,然后通過 UseCase 本身,最后通過 UseCase 輸出端口發送到 Presenter。
圖的主要部分(左邊)的箭頭表示組合和繼承 —— 組合用實心箭頭表示,繼承用空心箭頭表示。組合也被稱作 has-a 關系,繼承被稱作 is-a 關系。圓圈中的 “I” 和 “O” 表示輸入和輸出端口。可以看到,定義在綠色層中的 Controller,擁有一個(has-a)定義在紅色層中的輸入端口。UseCase(齒輪,業務邏輯,現在不重要)是一個(is-a)(或實現)輸入端口,并且擁有一個(has-a)輸出端口。最后,定義在綠色層中的 Presenter 實際上是一個(is-a)定義在紅色層的輸出端口。
現在,我們可以將其與數據流匹配。Controller 擁有一個輸入端口 —— 擁有一個指向它的引用。它調用輸入端口的一個方法,這樣數據就從 Controller 流到輸入端口。但輸入端口是一個接口,而它的實際實現是 UseCase。也就是說,它調用 UseCase 的一個方法,這樣數據就流向了 UseCase。UseCase 執行某些操作,并希望將數據發送回來。它擁有輸出端口的一個引用 —— 輸出端口定義在同一層 —— 因此它可以調用上面的方法。因此,數據流向輸出端口。最后 Presenter 是,或者實現了輸出端口,這是魔法的一部分。因為它實現了輸出端口,數據實際上流到它那了。
巧妙的是,UseCase 只知道它的輸出端口,世界在此停止(意指數據流到此結束)。Presenter 實現了它(輸出端口),實際上它可以被任何對象實現,因為 UseCase 不知道或不關心這些,它只清楚其層內的一畝三分地。可以看到,通過結合組合和繼承,我們可以使數據流向兩個方向,盡管內層并不知道它們在和外部世界通信。瞄一眼下圖:
可以看到,和依賴箭頭一樣,has-a 和 is-a 箭頭也指向中間。這是符合邏輯的。根據依賴規則,這是唯一可行的方法。外層可以看到內層,但不能反過來。唯一復雜的部分是,is-a 關系盡管指向了中間,卻反轉了數據流。
請注意,定義輸入和輸出端口是內層自己的職責,因此外層可以使用它們與其建立通信。我說過,這個解決方案先前已經暗示過,而且已經有了。那個講解抽象的通知例子,也是這種通信的一個例子。我們在內層定義了一個通知接口,業務邏輯可以用來向用戶顯示通知,但是我們在外層也定義一個實現。在這種情況下,通知接口是業務邏輯的輸出端口,用來和外部世界(在本例中,就是和具體的實現)通信。你不需要把你的類命名為 FooOutputPort 或者 BarInputPort,我們命名端口只是為了解釋理論。
總結
那么,它是過度復雜,過度費解的過度工程嗎?好吧,當你習慣了,它就簡單。并且這是必要的。它允許我們使得好的抽象/依賴實際匹配真實世界的通信和工作。也許這一切都提醒你不過是空中樓閣:美麗,理論上優雅,但過于復雜,我們仍然不知它是否有效,但在我們的案例中,它確實有效。
這就是本系列的第二部分。最后,第三部分,畢竟我們已經了解了理論和架構,將講解所有你需要了解的那些圖上的標簽。換句話說,分離的組件。我們將向你展示一個真實的應用于 Android 的 Clean Architecture。