在本系列的第一部分嗤练,我們介紹了我們在尋找可行架構的道路上所犯過的錯誤宜狐。在這部分呛梆,我們將介紹傳說中的 Clean Architecture而涉。
當你在谷歌搜索 "clean architecture" 時,你看到的第一張圖片是:
它也被稱為洋蔥架構速那,因為圖看起來象個洋蔥(你會意識到你需要寫樣板代碼寫到哭)劳淆;或者是端口和適配器凉翻,因為你可以看到右圖的一些端口兰伤。六角架構是另一個相似的架構内颗。
Clean Architecture 是前面提到的 Uncle Bob 的心血結晶,他是 《代碼整潔之道》的作者敦腔。這種方法的要點是均澳,業(yè)務邏輯(也稱為 domain),是宇宙的核心符衔。
掌控你的領域(domain)
當你打開項目時找前,你應該已經知道這個 app 是做什么的,與技術無關柏腻。其它一切都是實現細節(jié)纸厉。譬如系吭,持久化就是一個細節(jié)五嫂。定義接口,創(chuàng)建一個快速的粗糙的內存內(in-memory)實現肯尺,不要想太多沃缘,直到完成業(yè)務。然后你可以決定怎樣真正地持久化數據则吟。數據庫槐臀,網絡,兩者結合氓仲,文件系統(tǒng) —— 或者仍然保留在內存中水慨,或者結果你根本不需要持久化【纯福總之一句話:內層包含業(yè)務邏輯晰洒,外層包含實現細節(jié)。
話說回來啥箭, Clean Architectue 有一些特性使這成為可能:
- 依賴規(guī)則
- 抽象
- 層與層之間的通信
I.依賴規(guī)則
依賴規(guī)則可以用下圖解釋:
外層應該依賴內層谍珊。那三個在紅色框框內的箭頭表示依賴。與其使用“依賴”急侥,也許使用“看見”砌滞、“知道”侮邀、“了解”這類術語更好。在這些術語中贝润,外層看見绊茧,知道,了解內層题暖,但內層看不見按傅,也不知道,更不了解外層胧卤。正如我們先前所說唯绍,內存包含業(yè)務邏輯,外層包含實現細節(jié)枝誊。遵循依賴規(guī)則况芒,業(yè)務邏輯既看不到,也不知道叶撒,更不了解實現細節(jié)绝骚。這正是我們努力想要做到的。
如何實現依賴規(guī)則取決于你祠够。你可以把它們放到不同的包压汪,但小心“內層的”包不要使用“外層的”包。然而古瓤,如果有人不知道依賴規(guī)則止剖,沒有什么可以阻止他破壞規(guī)則。一個更好的方法是把層分離到不同的 Android 模塊(modules落君,即子項目)穿香,并在構建文件(build.grale)中調整依賴,這樣內層就無法依賴外層绎速。
還有值得一提的是皮获,雖然沒人可以阻止你跨層依賴,譬如藍色的層的組件使用紅色的層的組件纹冤,但我強烈建議你只訪問相鄰的層的組件洒宝。
II.抽象
抽象原則之前已有所暗示。也就是說萌京,當你朝圖中間移動時雁歌,東西變得更抽象。 這是有道理的:正如我們所說內層包含業(yè)務邏輯枫夺,而外層包含實現細節(jié)将宪。
甚至可以在多個層之間劃分相同的邏輯組件,如圖所示。 內層定義更抽象的部分较坛,外層定義更具體的部分印蔗。
舉個例子說清楚些。我們可以定義一個 “Notifications” 的抽象接口丑勤,并將其放到內層华嘹,這樣你的業(yè)務邏輯需要時可以使用它來向用戶顯示通知。另一方面法竞,我們可以這樣來實現該接口耙厚,即使用 Android NotificationManager 顯示通知來實現,并把該實現放到外層岔霸。
以這種方式薛躬,業(yè)務邏輯可以使用這樣的功能 —— 通知(在我們的例子中)—— 但它不了解實現細節(jié):實際的通知是如何實現的。此外呆细,業(yè)務邏輯甚至不知道實現細節(jié)的存在型宝。來看下面這張圖片:
當將抽象規(guī)則和依賴規(guī)則組合在一起時,結果是使用通知的抽象業(yè)務邏輯既不會看到絮爷,也不會知道趴酣,更不會了解使用 Android NotificationManager 的具體實現。這很好坑夯,因為我們可以在業(yè)務邏輯毫不知情的情況下切換具體實現岖寞。
讓我們把這種規(guī)則組合和標準的三層架構簡單對比下,看看它們各自的抽象和依賴是怎樣的以及如何工作的柜蜈。
在圖中仗谆,你可以看到,標準三層架構的所有依賴最終都傳到數據庫跨释。也就是說胸私,抽象和依賴并不匹配厌处。在邏輯上鳖谈,業(yè)務層應該是 app 的中心,但它卻不是阔涉,因為依賴朝向數據庫缆娃。
業(yè)務層不應該知道數據庫,應該反過來瑰排。在 Clean Architecture 中贯要,依賴朝向業(yè)務層(內層),并且抽象也提升到業(yè)務層椭住,因此它們很好地匹配崇渗。
這是重要的,因為抽象是理論,依賴是實踐宅广。抽象是 app 的邏輯布局葫掉,依賴關系是(組件)如何實際組合在一起。在 Clean Architecture 中跟狱,這兩者是匹配的俭厚。而在標準三層架構中則不然,如果你不小心驶臊,很容易導致各種邏輯上的不一致和混亂挪挤。
III.層與層之間的通信
現在我們將 app 分模塊,將所有內容分開关翎,將業(yè)務邏輯放在我們 app 的中心扛门,并在外層實現細節(jié),一切看起來都很棒纵寝。 但是你可能很快遇到一個有趣的問題尖飞。
如果你的 UI 是一個實現細節(jié),網絡是一個實現細節(jié)店雅,業(yè)務邏輯在中間政基,那么我們如何從互聯(lián)網獲取數據,經過業(yè)務邏輯闹啦,然后發(fā)送到界面沮明?
業(yè)務邏輯在中間,應該協(xié)調網絡和界面窍奋,但它甚至不知道兩者的存在荐健。這是一個關于通信和數據流的問題。
我們希望數據能夠從外層流向內層琳袄,反之亦然江场,但依賴規(guī)則不允許。 讓我們舉個最簡單的例子窖逗。
我們只有兩層址否,綠色和紅色的。綠色的是外層碎紊,它知道紅色的佑附,紅色的是內層,它只知道自己仗考。我們希望數據從綠色流向紅色音同,然后折回綠色。該解決方案先前已經暗示過了秃嗜,看下圖:
圖的右邊部分顯示了數據流权均。數據源于 Controller顿膨,經過 UseCase(或者替換成你選擇的組件)的輸入端口,然后通過 UseCase 本身叽赊,最后通過 UseCase 輸出端口發(fā)送到 Presenter虽惭。
圖的主要部分(左邊)的箭頭表示組合和繼承 —— 組合用實心箭頭表示,繼承用空心箭頭表示蛇尚。組合也被稱作 has-a 關系芽唇,繼承被稱作 is-a 關系。圓圈中的 “I” 和 “O” 表示輸入和輸出端口取劫〈殷裕可以看到,定義在綠色層中的 Controller谱邪,擁有一個(has-a)定義在紅色層中的輸入端口炮捧。UseCase(齒輪,業(yè)務邏輯惦银,現在不重要)是一個(is-a)(或實現)輸入端口咆课,并且擁有一個(has-a)輸出端口。最后扯俱,定義在綠色層中的 Presenter 實際上是一個(is-a)定義在紅色層的輸出端口书蚪。
現在,我們可以將其與數據流匹配迅栅。Controller 擁有一個輸入端口 —— 擁有一個指向它的引用殊校。它調用輸入端口的一個方法,這樣數據就從 Controller 流到輸入端口读存。但輸入端口是一個接口为流,而它的實際實現是 UseCase。也就是說让簿,它調用 UseCase 的一個方法敬察,這樣數據就流向了 UseCase。UseCase 執(zhí)行某些操作尔当,并希望將數據發(fā)送回來莲祸。它擁有輸出端口的一個引用 —— 輸出端口定義在同一層 —— 因此它可以調用上面的方法。因此居凶,數據流向輸出端口虫给。最后 Presenter 是藤抡,或者實現了輸出端口侠碧,這是魔法的一部分。因為它實現了輸出端口缠黍,數據實際上流到它那了弄兜。
巧妙的是,UseCase 只知道它的輸出端口,世界在此停止(意指數據流到此結束)替饿。Presenter 實現了它(輸出端口)语泽,實際上它可以被任何對象實現,因為 UseCase 不知道或不關心這些视卢,它只清楚其層內的一畝三分地踱卵。可以看到据过,通過結合組合和繼承惋砂,我們可以使數據流向兩個方向,盡管內層并不知道它們在和外部世界通信绳锅。瞄一眼下圖:
可以看到西饵,和依賴箭頭一樣,has-a 和 is-a 箭頭也指向中間鳞芙。這是符合邏輯的眷柔。根據依賴規(guī)則,這是唯一可行的方法原朝。外層可以看到內層驯嘱,但不能反過來。唯一復雜的部分是喳坠,is-a 關系盡管指向了中間宙拉,卻反轉了數據流。
請注意丙笋,定義輸入和輸出端口是內層自己的職責谢澈,因此外層可以使用它們與其建立通信。我說過御板,這個解決方案先前已經暗示過锥忿,而且已經有了。那個講解抽象的通知例子怠肋,也是這種通信的一個例子敬鬓。我們在內層定義了一個通知接口,業(yè)務邏輯可以用來向用戶顯示通知笙各,但是我們在外層也定義一個實現钉答。在這種情況下,通知接口是業(yè)務邏輯的輸出端口杈抢,用來和外部世界(在本例中数尿,就是和具體的實現)通信。你不需要把你的類命名為 FooOutputPort 或者 BarInputPort惶楼,我們命名端口只是為了解釋理論右蹦。
總結
那么诊杆,它是過度復雜,過度費解的過度工程嗎何陆?好吧晨汹,當你習慣了,它就簡單贷盲。并且這是必要的淘这。它允許我們使得好的抽象/依賴實際匹配真實世界的通信和工作。也許這一切都提醒你不過是空中樓閣:美麗巩剖,理論上優(yōu)雅慨灭,但過于復雜,我們仍然不知它是否有效球及,但在我們的案例中氧骤,它確實有效。
這就是本系列的第二部分吃引。最后筹陵,第三部分,畢竟我們已經了解了理論和架構镊尺,將講解所有你需要了解的那些圖上的標簽朦佩。換句話說,分離的組件庐氮。我們將向你展示一個真實的應用于 Android 的 Clean Architecture语稠。