到目前為止,在這個系列中,我們已經(jīng)講解了一些 初學(xué)者易犯的錯誤,以及過了一遍 Clean Architecture。在這最后一部分,我們會介紹最后一個難題:標(biāo)簽,或者準(zhǔn)確地說,組件。
譯者注:看完了這一部分,還有第四部分。在第四部分將會提供一個很酷的示范項(xiàng)目。
首先,我會移除在 Android 項(xiàng)目中不使用的東西,添加一些在 Uncle
Bob 的圖中找不到的但我們需要使用的東西,看起來像這樣:
我會從中心(抽象)講到邊緣(具體)。
Entities
實(shí)體(又稱領(lǐng)域?qū)ο蠡驑I(yè)務(wù)對象)是 app 的核心。它們代表 app 的主要功能,你應(yīng)該能夠僅通過查看實(shí)體來說出 app 是做什么的。它們包含業(yè)務(wù)邏輯 —— 僅限于驗(yàn)證和類似的東西。它們不和真實(shí)的外部世界交互,也不會處理持久化。如果你有一個新聞 app,那么實(shí)體就是類別、文章和商業(yè)。
Use Cases
用例,也稱為 interactor(交互器),也可以叫做 business service (業(yè)務(wù)服務(wù)對象),是實(shí)體的擴(kuò)展,是 business logic(業(yè)務(wù)邏輯)的擴(kuò)展。也就是說,它們包含的業(yè)務(wù)邏輯不限于一個實(shí)體,而是可以處理很多實(shí)體。一個好用例的標(biāo)準(zhǔn)是,你可以使用一句簡單的常用語言來描述它是做什么的,例如,“把錢從一個賬戶轉(zhuǎn)移到另一個賬戶”。你甚至可以使用這樣的命名系統(tǒng)來命名該類,例如 TransferMoneyUseCase。
Repositories
倉庫用于持久化實(shí)體。就這么簡單。它們定義為接口,并且用在要對實(shí)體執(zhí)行增刪查改操作的用例的輸出端口。此外,它們可以公開一些與持久化相關(guān)的復(fù)雜操作,譬如過濾、聚合等。具體持久化策略,譬如數(shù)據(jù)庫或網(wǎng)絡(luò),在外層中實(shí)現(xiàn)。例如,你可以把接口命名為 AccountRepository。
Presenters
如果你熟悉 MVP 模式,Presenter 做你想讓它做的事情。它們處理用戶交互,調(diào)用恰當(dāng)?shù)臉I(yè)務(wù)邏輯,并將數(shù)據(jù)發(fā)送給 UI 渲染。這里通常有各種類型的模型之間的映射轉(zhuǎn)換。有些人會在這里使用控制器,這是可以的。我們使用的 Presenter 被正式稱為監(jiān)督控制器,我們通常根據(jù)屏幕方向?yàn)槊總€界面定義一個或兩個 Presenter,并且 Presenter 的生命周期和相關(guān)的 View 的生命周期綁定。一個建議:嘗試以技術(shù)無關(guān)的方式命名 Presenter 中的方法,假裝你不知道 View 是用什么技術(shù)實(shí)現(xiàn)的。所以,如果在 View 中有方法名為 onSubmitOrderButtonClicked 和 onUserListItemSelected,那么處理這些事件的相應(yīng)的 Presenter 中的方法可以被命名為 submitOrder 和 selectUser。
Device
這個組件在先前通知那個例子中已經(jīng)被玩壞了。它包含了諸如傳感器、鬧鐘、通知、播放器、各種 *Manager 等等真實(shí) Android 功能的實(shí)現(xiàn)。它包含兩部分組件。第一部分是定義在內(nèi)層的接口,業(yè)務(wù)邏輯用它來作為和外部世界通信的輸出端口。第二部分,也畫在圖中,是那些接口的實(shí)現(xiàn)。因此,比如,你可以定義名為 Gyroscope, Alarm, Notifications, 和 Player 的接口。請注意,這些名稱是抽象的技術(shù)無關(guān)的。業(yè)務(wù)邏輯不關(guān)心通知如何顯示,播放器如何播放聲音,或螺旋儀的數(shù)據(jù)來自哪里。你可以創(chuàng)建一個將通知寫入終端,將聲音數(shù)據(jù)寫到日志,或者從預(yù)先定義好的文件中收集螺旋儀數(shù)據(jù)的實(shí)現(xiàn)。這樣的實(shí)現(xiàn)對于調(diào)試或創(chuàng)建一個用于你編碼的確定性的環(huán)境是很有用的。當(dāng)然,你必須創(chuàng)建諸如 AndroidAlarm,NativePlayer 等等的實(shí)現(xiàn)。在大多數(shù)情況下,這些實(shí)現(xiàn)僅僅是 Android Manager 類的包裝。
DB & API
這里沒有哲學(xué)。將倉庫的實(shí)現(xiàn)放在此組件中。所有的底層持久化的東西應(yīng)該放在這里:DAO,ORM,Retrofit(或別的),JSON 解析等等。你還可以在這里實(shí)現(xiàn)緩存策略或者簡單地在內(nèi)存中(in-memory)持久化,直到你完成了 app 的其余部分。我們團(tuán)隊(duì)最近進(jìn)行了一個有趣的討論。問題是這樣:倉庫是否應(yīng)該公開諸如 fetchUsersOffline(fetchUsersFromCache)和 fetchUsersOnline(fetchUsersFromInternet)之類的方法?換句話說,業(yè)務(wù)邏輯是否應(yīng)該知道數(shù)據(jù)來自哪里。讀完這篇文章的所有內(nèi)容后,答案很簡單:不。但這里有個陷阱。如果關(guān)于數(shù)據(jù)源的決策是業(yè)務(wù)邏輯的一部分 —— 譬如,用戶可以選擇或者 app 有一個明確的離線模式 —— 然后你可以添加這樣的區(qū)分。但我不會為每個請求定義兩種方法。我可能會在倉庫中公開 enterOfflineMode 和 exitOfflineMode 這樣的方法。或者如果它適用于所有倉庫,我們可以使用 enter 和 exit 方法定義一個 OfflineMode 接口,并在業(yè)務(wù)端使用它,讓倉庫去查詢它的模式并且在內(nèi)部決策。
UI
這里的哲學(xué)更少。將和 Android UI 相關(guān)的東西放在這里。Activity、Fragment、View、Adapter 等等。完了。
模塊(Modules)
下圖顯示了我們?nèi)绾螌⑺羞@些組件分解成 Android Studio 模塊。 你可能會發(fā)現(xiàn)另一種更合適的分法。
我們將實(shí)體、用例、倉庫和設(shè)備接口分到領(lǐng)域模塊。如果你想要一個額外的挑戰(zhàn)(獎勵是永恒的榮耀和完全整潔的設(shè)計(jì)),你可以使該模塊成為一個純 java 模塊。這將阻止你走捷徑將一些 Android 相關(guān)的東西放在這里。
設(shè)備模塊包含所有和 Android 相關(guān)的東西(除了數(shù)據(jù)持久化和 UI)。數(shù)據(jù)模塊應(yīng)該持有和數(shù)據(jù)持久化相關(guān)的東西,正如我們說過的那樣。你不能把這兩者弄成 java 模塊,因?yàn)樗鼈冃枰L問各種 Andriod 相關(guān)的東西。你可以把它們弄成 Android library。
最后,我們將和 UI (包括 Presenter)相關(guān)的所有東西分到 UI 模塊。你可以明確地將其命名為 UI,但是由于所有 Android 的東西都在這里,我們保留它 “app” 的名字,正如 Android Studio 在創(chuàng)建項(xiàng)目時所命名的那樣。
好點(diǎn)了嗎?
為了回答這個問題,我丟開 Uncle Bob 的圖,將先前描述的組件攤開到圖中,就像之前那些我們曾經(jīng)評估過的架構(gòu)類型那樣。這樣做之后,我們得到:
現(xiàn)在讓我們來使用與之前的架構(gòu)相同的評價標(biāo)準(zhǔn)。
它完全分離到模塊級別、包級別、類級別,所以應(yīng)該滿足單一職責(zé)原則。
我們已經(jīng)將 Android 和真實(shí)世界的東西盡可能地推到邊緣,業(yè)務(wù)邏輯再也沒有直接接觸 Android。
我們很好地分離類以方便測試。接觸 Android 世界的類可以使用 Android 測試?yán)M(jìn)行測試,沒有接觸的類可以使用 JUnit 進(jìn)行測試。可能有人惡意稱它為類爆炸,我稱之為可測試。:)
這可能很復(fù)雜——但值得
我希望我精心挑選的標(biāo)準(zhǔn),不僅能迎合 Clean Architecture,也能說服你一試。看起來很復(fù)雜,而且有很多細(xì)節(jié),但這是值得的。一旦你把所有都串起來,測試就會變的更容易,BUG 更容易定位,新功能更容易添加,代碼更易讀和維護(hù),一切都可以完美運(yùn)行,宇宙都被滿足了。
所以,就是這樣。如果你還沒有這樣做,請看本系列先前的文章:初學(xué)者易犯的錯誤 和 介紹 Clean Architecture。如果你有任何意見或問題,請留言。我們總是有興趣聽到你的想法。
閱讀 Andriod 架構(gòu)系列 第四部分