1. 程式人生 > >一種更清晰的 Android 架構

一種更清晰的 Android 架構

過去幾個月以來,通過在Tuenti網站上與@pedro_g_s和@flipper83(安卓開發兩位大牛)進行友好討論之後,我決定寫這篇關於架構安卓應用的文章。

我寫這篇文章的目的是想把我在過去幾個月體悟到的小方法以及在調查和應用中學到的有用的東西分享給大家。
入門指南

大家都知道要寫一款精品軟體是有難度且很複雜的:不僅要滿足特定要求,而且軟體還必須具有穩健性,可維護、可測試性強,並且能夠靈活適應各種發展與變化。這時候,“清晰架構”就應運而生了,這一架構在開發任何軟體應用的時候用起來非常順手。

這個思路很簡單:簡潔架構 意味著產品系統中遵循一系列的習慣原則:

框架獨立性
可測試
UI獨立性
資料庫獨立性
任何外部代理模組的獨立性

arch

我們並不要求一定要用四環結構(如圖所示),這只是一個示例圖解,但是要考慮的是依賴項規則:原始碼依賴項只能向內指向,內環裡的所有項不能瞭解外環所發生的東西。

以下是更好地理解和熟悉本方法的一些相關詞彙:

Entities:是指一款應用的業務物件
Use cases:是指結合資料流和實體中的用例,也稱為Interactor
Interface Adapters: 這一組介面卡,是負責以最合理的格式轉換用例(use cases)和實體(entities)之間的資料,表現層(Presenters )和控制層(Controllers ),就屬於這一塊的。
Frameworks and Drivers: 這裡是所有具體的實現了:比如:UI,工具類,基礎框架,等等。

想要更具體,更生動豐富的解釋,可以參考這篇文章或者這個視訊。
場景

我會設定一個簡單的場景來開始:建立一個簡單的小app,app中顯示從雲端獲取的一個朋友或使用者列表。當點選其中任何一個時,會開啟一個新的視窗,顯示該使用者的詳細資訊。這裡我放了一段視訊,大家看看這個視訊 (需翻牆)大概就可以對我所描述的東西瞭解個大概了。
Android應用架構

這一物件遵循關注分離原則,也就是通過業務規則讓內環操作對外環事物一無所知,這樣一來,在測試時它們就不會依賴任何的外部元素了。
要達到這個目的,我的建議就是把一個專案分成三個層次,每個層次擁有自己的目的並且各自獨立於堆放運作。
值得一提的是,每一層次使用其自有的資料模型以達到獨立性的目的(大家可以看到,在程式碼中需要一個數據對映器來完成資料轉換。如果你不想把你的模型和整個應用交叉使用,這是你要付出的代價)。

以下是圖解,大家感受下:

schema

注:我並沒有使用任何的外部庫(除了用於json資料句法分析的gson和用於測試的junit, mockito, robolectric和espresso以外)。原因是它可以使這個示例更清晰。總之,在儲存磁碟資料時,記得加上ORM、依賴注入框架或者你熟悉的任何工具或庫,這些都會帶來很大幫助。(記住:重複製造輪子可不是明智的選擇)

表現層 (Presentation Layer)

表現層在此,表現的是與檢視和動畫相關的邏輯。這裡僅用了一個Model View Presenter(下文簡稱MVP),但是大家也可以用MVC或MVVM等模式。這裡我不再贅述細節,但是需要強調的是,這裡的fragment和activity都是View,其內部除了UI邏輯以外沒有其他邏輯,這也是所有渲染的東西發生的地方。
本層次的Presenter由多個interactor(用例)組成,用於完成Android UI執行緒以外的新執行緒的工作,並藉助渲染到view中的資料callback函式來返回。

mvp

如果你需要一個使用MVP和MVVM的Effective Android UI典型案例,可以參考我朋友Pedro Gómez的文章。
領域層 (Domain Layer)

這裡的業務規則是指所有在本層發生的邏輯。對於Android專案來說,大家還可以看到所有的interactor(用例)實施。這一層是純粹的java模組,沒有任何的Android依賴性。當涉及到業務物件時,所有的外部元件都使用介面。

domain
資料層 (Data Layer)

應用所需的所有資料都來自這一層中的UserRepository實現(介面在領域層)。這一實現採用了Repository Pattern,主要策略是通過一個工廠根據一定的條件選取不同的資料來源。
比如,通過ID獲取一個使用者時,如果這個使用者在快取中已經存在,則硬碟快取資料來源會被選中,否則會通過向雲端發起請求獲取資料,然後儲存到硬碟快取。
這一切背後的原理是由於原始資料對於客戶端是透明的,客戶端並不關心資料是來源於記憶體、硬碟還是雲端,它需要關心的是資料可以正確地獲取到。

data

注:在程式碼方面,出於學習目的,我通過檔案系統和Android preference實現了一個簡單、原始的硬碟快取。請記住,如果已經存在了能夠完成這些工作的庫,就不要重複製造輪子。

錯誤處理

這是一個長期待解決的討論話題,如果大家能夠分享各自的解決方案,那真真是極好的。
我的策略是使用回撥,這樣的話,如果資料倉庫發生了變化,回撥有兩個方法:onResponse()和onError(). onError方法將異常資訊封裝到一個ErrorBundle物件中: 這種方法的難點在於這其中會存在一環扣一環的回撥鏈,錯誤會沿著這條回撥鏈到達展示層。因此會犧牲一點程式碼的可讀性。另外,如果出現錯誤,我本來可以通過事件匯流排系統丟擲事件,但是這種實現方式類似於使用C語言的goto語法。在我看來,當你訂閱多個事件時,如果不能很好的控制,你可能會被弄得暈頭轉向。
測試

關於測試方面,我根據不同的層來選擇不同的方法:

展示層 ( Presentation Layer) : 使用android instrumentation和 espresso進行整合和功能測試
領域層 ( Domain Layer) : 使用JUnit和Mockito進行單元測試;
資料層 ( Data Layer) : 使用Robolectric ( 因為依賴於Android SDK中的類 )進行整合測試和單元測試。

程式碼展示

我猜你現在在想,扯了那麼久的淡,程式碼究竟在哪裡呢? 好吧,這就是你可以找到上述解決方案的github連結。還要提一點,在資料夾結構方面,不同的層是通過以下不同的模組反應的:

presentation: 展示層的Android模組
domain: 一個沒有android依賴的java模組
data: 一個數據獲取來源的android模組。
data-test: 資料層測試,由於使用Robolectric 存在一些限制,所以我得再獨立的java模組中使用。

結論

正如 Bob大叔 所說:“Architecture is About Intent, not Frameworks” ,我非常同意這個說法,當然了,有很多不同的方法做不同的事情(不同的實現方法),我很確定,你每天(像我一樣)會面臨很多挑戰,但是遵循這些方法,可以確保你的應用會:

易維護
易測試
高內聚
低耦合

最後,我強烈推薦你去實踐一下,並且分享你的經驗。也許你會找到更好的解決方案:我們都知道,不斷提升自己是一件件非常好的事。我希望這篇文章對你有所幫助,歡迎拍磚。