[譯] Android 架構(gòu):Part 2 —— 介紹 Clean Architecture

在本系列的第一部分,我們介紹了我們在尋找可行架構(gòu)的道路上所犯過的錯誤。在這部分,我們將介紹傳說中的 Clean Architecture。

當(dāng)你在谷歌搜索 "clean architecture" 時,你看到的第一張圖片是:

它也被稱為洋蔥架構(gòu),因?yàn)閳D看起來象個洋蔥(你會意識到你需要寫樣板代碼寫到哭);或者是端口和適配器,因?yàn)槟憧梢钥吹接覉D的一些端口。六角架構(gòu)是另一個相似的架構(gòu)。

Clean Architecture 是前面提到的 Uncle Bob 的心血結(jié)晶,他是 《代碼整潔之道》的作者。這種方法的要點(diǎn)是,業(yè)務(wù)邏輯(也稱為 domain),是宇宙的核心。

掌控你的領(lǐng)域(domain)

當(dāng)你打開項(xiàng)目時,你應(yīng)該已經(jīng)知道這個 app 是做什么的,與技術(shù)無關(guān)。其它一切都是實(shí)現(xiàn)細(xì)節(jié)。譬如,持久化就是一個細(xì)節(jié)。定義接口,創(chuàng)建一個快速的粗糙的內(nèi)存內(nèi)(in-memory)實(shí)現(xiàn),不要想太多,直到完成業(yè)務(wù)。然后你可以決定怎樣真正地持久化數(shù)據(jù)。數(shù)據(jù)庫,網(wǎng)絡(luò),兩者結(jié)合,文件系統(tǒng) —— 或者仍然保留在內(nèi)存中,或者結(jié)果你根本不需要持久化??傊痪湓挘簝?nèi)層包含業(yè)務(wù)邏輯,外層包含實(shí)現(xiàn)細(xì)節(jié)。

話說回來, Clean Architectue 有一些特性使這成為可能:

  1. 依賴規(guī)則
  2. 抽象
  3. 層與層之間的通信

I.依賴規(guī)則

依賴規(guī)則可以用下圖解釋:

外層應(yīng)該依賴內(nèi)層。那三個在紅色框框內(nèi)的箭頭表示依賴。與其使用“依賴”,也許使用“看見”、“知道”、“了解”這類術(shù)語更好。在這些術(shù)語中,外層看見,知道,了解內(nèi)層,但內(nèi)層看不見,也不知道,更不了解外層。正如我們先前所說,內(nèi)存包含業(yè)務(wù)邏輯,外層包含實(shí)現(xiàn)細(xì)節(jié)。遵循依賴規(guī)則,業(yè)務(wù)邏輯既看不到,也不知道,更不了解實(shí)現(xiàn)細(xì)節(jié)。這正是我們努力想要做到的。

如何實(shí)現(xiàn)依賴規(guī)則取決于你。你可以把它們放到不同的包,但小心“內(nèi)層的”包不要使用“外層的”包。然而,如果有人不知道依賴規(guī)則,沒有什么可以阻止他破壞規(guī)則。一個更好的方法是把層分離到不同的 Android 模塊(modules,即子項(xiàng)目),并在構(gòu)建文件(build.grale)中調(diào)整依賴,這樣內(nèi)層就無法依賴外層。

還有值得一提的是,雖然沒人可以阻止你跨層依賴,譬如藍(lán)色的層的組件使用紅色的層的組件,但我強(qiáng)烈建議你只訪問相鄰的層的組件。

II.抽象

抽象原則之前已有所暗示。也就是說,當(dāng)你朝圖中間移動時,東西變得更抽象。 這是有道理的:正如我們所說內(nèi)層包含業(yè)務(wù)邏輯,而外層包含實(shí)現(xiàn)細(xì)節(jié)。

甚至可以在多個層之間劃分相同的邏輯組件,如圖所示。 內(nèi)層定義更抽象的部分,外層定義更具體的部分。

舉個例子說清楚些。我們可以定義一個 “Notifications” 的抽象接口,并將其放到內(nèi)層,這樣你的業(yè)務(wù)邏輯需要時可以使用它來向用戶顯示通知。另一方面,我們可以這樣來實(shí)現(xiàn)該接口,即使用 Android NotificationManager 顯示通知來實(shí)現(xiàn),并把該實(shí)現(xiàn)放到外層。

以這種方式,業(yè)務(wù)邏輯可以使用這樣的功能 —— 通知(在我們的例子中)—— 但它不了解實(shí)現(xiàn)細(xì)節(jié):實(shí)際的通知是如何實(shí)現(xiàn)的。此外,業(yè)務(wù)邏輯甚至不知道實(shí)現(xiàn)細(xì)節(jié)的存在。來看下面這張圖片:

當(dāng)將抽象規(guī)則和依賴規(guī)則組合在一起時,結(jié)果是使用通知的抽象業(yè)務(wù)邏輯既不會看到,也不會知道,更不會了解使用 Android NotificationManager 的具體實(shí)現(xiàn)。這很好,因?yàn)槲覀兛梢栽跇I(yè)務(wù)邏輯毫不知情的情況下切換具體實(shí)現(xiàn)。

讓我們把這種規(guī)則組合和標(biāo)準(zhǔn)的三層架構(gòu)簡單對比下,看看它們各自的抽象和依賴是怎樣的以及如何工作的。

在圖中,你可以看到,標(biāo)準(zhǔn)三層架構(gòu)的所有依賴最終都傳到數(shù)據(jù)庫。也就是說,抽象和依賴并不匹配。在邏輯上,業(yè)務(wù)層應(yīng)該是 app 的中心,但它卻不是,因?yàn)橐蕾嚦驍?shù)據(jù)庫。

業(yè)務(wù)層不應(yīng)該知道數(shù)據(jù)庫,應(yīng)該反過來。在 Clean Architecture 中,依賴朝向業(yè)務(wù)層(內(nèi)層),并且抽象也提升到業(yè)務(wù)層,因此它們很好地匹配。

這是重要的,因?yàn)槌橄笫抢碚摚蕾囀菍?shí)踐。抽象是 app 的邏輯布局,依賴關(guān)系是(組件)如何實(shí)際組合在一起。在 Clean Architecture 中,這兩者是匹配的。而在標(biāo)準(zhǔn)三層架構(gòu)中則不然,如果你不小心,很容易導(dǎo)致各種邏輯上的不一致和混亂。

III.層與層之間的通信

現(xiàn)在我們將 app 分模塊,將所有內(nèi)容分開,將業(yè)務(wù)邏輯放在我們 app 的中心,并在外層實(shí)現(xiàn)細(xì)節(jié),一切看起來都很棒。 但是你可能很快遇到一個有趣的問題。

如果你的 UI 是一個實(shí)現(xiàn)細(xì)節(jié),網(wǎng)絡(luò)是一個實(shí)現(xiàn)細(xì)節(jié),業(yè)務(wù)邏輯在中間,那么我們?nèi)绾螐幕ヂ?lián)網(wǎng)獲取數(shù)據(jù),經(jīng)過業(yè)務(wù)邏輯,然后發(fā)送到界面?

業(yè)務(wù)邏輯在中間,應(yīng)該協(xié)調(diào)網(wǎng)絡(luò)和界面,但它甚至不知道兩者的存在。這是一個關(guān)于通信和數(shù)據(jù)流的問題。

我們希望數(shù)據(jù)能夠從外層流向內(nèi)層,反之亦然,但依賴規(guī)則不允許。 讓我們舉個最簡單的例子。

我們只有兩層,綠色和紅色的。綠色的是外層,它知道紅色的,紅色的是內(nèi)層,它只知道自己。我們希望數(shù)據(jù)從綠色流向紅色,然后折回綠色。該解決方案先前已經(jīng)暗示過了,看下圖:

圖的右邊部分顯示了數(shù)據(jù)流。數(shù)據(jù)源于 Controller,經(jīng)過 UseCase(或者替換成你選擇的組件)的輸入端口,然后通過 UseCase 本身,最后通過 UseCase 輸出端口發(fā)送到 Presenter。

圖的主要部分(左邊)的箭頭表示組合和繼承 —— 組合用實(shí)心箭頭表示,繼承用空心箭頭表示。組合也被稱作 has-a 關(guān)系,繼承被稱作 is-a 關(guān)系。圓圈中的 “I” 和 “O” 表示輸入和輸出端口。可以看到,定義在綠色層中的 Controller,擁有一個(has-a)定義在紅色層中的輸入端口。UseCase(齒輪,業(yè)務(wù)邏輯,現(xiàn)在不重要)是一個(is-a)(或?qū)崿F(xiàn))輸入端口,并且擁有一個(has-a)輸出端口。最后,定義在綠色層中的 Presenter 實(shí)際上是一個(is-a)定義在紅色層的輸出端口。

現(xiàn)在,我們可以將其與數(shù)據(jù)流匹配。Controller 擁有一個輸入端口 —— 擁有一個指向它的引用。它調(diào)用輸入端口的一個方法,這樣數(shù)據(jù)就從 Controller 流到輸入端口。但輸入端口是一個接口,而它的實(shí)際實(shí)現(xiàn)是 UseCase。也就是說,它調(diào)用 UseCase 的一個方法,這樣數(shù)據(jù)就流向了 UseCase。UseCase 執(zhí)行某些操作,并希望將數(shù)據(jù)發(fā)送回來。它擁有輸出端口的一個引用 —— 輸出端口定義在同一層 —— 因此它可以調(diào)用上面的方法。因此,數(shù)據(jù)流向輸出端口。最后 Presenter 是,或者實(shí)現(xiàn)了輸出端口,這是魔法的一部分。因?yàn)樗鼘?shí)現(xiàn)了輸出端口,數(shù)據(jù)實(shí)際上流到它那了。

巧妙的是,UseCase 只知道它的輸出端口,世界在此停止(意指數(shù)據(jù)流到此結(jié)束)。Presenter 實(shí)現(xiàn)了它(輸出端口),實(shí)際上它可以被任何對象實(shí)現(xiàn),因?yàn)?UseCase 不知道或不關(guān)心這些,它只清楚其層內(nèi)的一畝三分地??梢钥吹?,通過結(jié)合組合和繼承,我們可以使數(shù)據(jù)流向兩個方向,盡管內(nèi)層并不知道它們在和外部世界通信。瞄一眼下圖:

可以看到,和依賴箭頭一樣,has-a 和 is-a 箭頭也指向中間。這是符合邏輯的。根據(jù)依賴規(guī)則,這是唯一可行的方法。外層可以看到內(nèi)層,但不能反過來。唯一復(fù)雜的部分是,is-a 關(guān)系盡管指向了中間,卻反轉(zhuǎn)了數(shù)據(jù)流。

請注意,定義輸入和輸出端口是內(nèi)層自己的職責(zé),因此外層可以使用它們與其建立通信。我說過,這個解決方案先前已經(jīng)暗示過,而且已經(jīng)有了。那個講解抽象的通知例子,也是這種通信的一個例子。我們在內(nèi)層定義了一個通知接口,業(yè)務(wù)邏輯可以用來向用戶顯示通知,但是我們在外層也定義一個實(shí)現(xiàn)。在這種情況下,通知接口是業(yè)務(wù)邏輯的輸出端口,用來和外部世界(在本例中,就是和具體的實(shí)現(xiàn))通信。你不需要把你的類命名為 FooOutputPort 或者 BarInputPort,我們命名端口只是為了解釋理論。

總結(jié)

那么,它是過度復(fù)雜,過度費(fèi)解的過度工程嗎?好吧,當(dāng)你習(xí)慣了,它就簡單。并且這是必要的。它允許我們使得好的抽象/依賴實(shí)際匹配真實(shí)世界的通信和工作。也許這一切都提醒你不過是空中樓閣:美麗,理論上優(yōu)雅,但過于復(fù)雜,我們?nèi)匀徊恢欠裼行?,但在我們的案例中,它確實(shí)有效。

這就是本系列的第二部分。最后,第三部分,畢竟我們已經(jīng)了解了理論和架構(gòu),將講解所有你需要了解的那些圖上的標(biāo)簽。換句話說,分離的組件。我們將向你展示一個真實(shí)的應(yīng)用于 Android 的 Clean Architecture。

原文

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容