一種更清晰的Android架構
- 原文連結 : Architecting Android…The clean way?
- 譯者 : Mr.Simple & Sophie.Ping
過去幾個月以來,通過在Tuenti網站上與@pedro_g_s和@flipper83(安卓開發兩位大牛)進行友好讨論之後,我決定寫這篇關于架構安卓應用的文章。
我寫這篇文章的目的是想把我在過去幾個月體悟到的小方法以及在調查和應用中學到的有用的東西分享給大家。
入門指南
大家都知道要寫一款精品軟體是有難度且很複雜的:不僅要滿足特定要求,而且軟體還必須具有穩健性,可維護、可測試性強,并且能夠靈活适應各種發展與變化。這時候,“清晰架構”就應運而生了,這一架構在開發任何軟體應用的時候用起來非常順手。
這個思路很簡單:簡潔架構 意味着産品系統中遵循一系列的習慣原則:
- 架構獨立性
- 可測試
- UI獨立性
- 資料庫獨立性
- 任何外部代理子產品的獨立性
我們并不要求一定要用四環結構(如圖所示),這隻是一個示例圖解,但是要考慮的是依賴項規則:源碼依賴項隻能向内指向,内環裡的所有項不能了解外環所發生的東西。
以下是更好地了解和熟悉本方法的一些相關詞彙:
- Entities:是指一款應用的業務對象
- Use cases:是指結合資料流和實體中的用例,也稱為Interactor
- Interface Adapters: 這一組擴充卡,是負責以最合理的格式轉換用例(use cases)和實體(entities)之間的資料,表現層(Presenters )和控制層(Controllers ),就屬于這一塊的。
- Frameworks and Drivers: 這裡是所有具體的實作了:比如:UI,工具類,基礎架構,等等。
想要更具體,更生動豐富的解釋,可以參考這篇文章或者這個視訊。
場景
我會設定一個簡單的場景來開始:建立一個簡單的小app,app中顯示從雲端擷取的一個朋友或使用者清單。當點選其中任何一個時,會打開一個新的視窗,顯示該使用者的詳細資訊。這裡我放了一段視訊,大家看看這個視訊 (需翻牆)大概就可以對我所描述的東西了解個大概了。
Android應用架構
這一對象遵循關注分離原則,也就是通過業務規則讓内環操作對外環事物一無所知,這樣一來,在測試時它們就不會依賴任何的外部元素了。
要達到這個目的,我的建議就是把一個項目分成三個層次,每個層次擁有自己的目的并且各自獨立于堆放運作。
值得一提的是,每一層次使用其自有的資料模型以達到獨立性的目的(大家可以看到,在代碼中需要一個資料映射器來完成資料轉換。如果你不想把你的模型和整個應用交叉使用,這是你要付出的代價)。
以下是圖解,大家感受下:
注:我并沒有使用任何的外部庫(除了用于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和MVVM的Effective Android UI典型案例,可以參考我朋友Pedro Gómez的文章。
領域層 (Domain Layer)
這裡的業務規則是指所有在本層發生的邏輯。對于Android項目來說,大家還可以看到所有的interactor(用例)實施。這一層是純粹的java子產品,沒有任何的Android依賴性。當涉及到業務對象時,所有的外部元件都使用接口。
資料層 (Data Layer)
應用所需的所有資料都來自這一層中的UserRepository實作(接口在領域層)。這一實作采用了Repository Pattern,主要政策是通過一個工廠根據一定的條件選取不同的資料來源。
比如,通過ID擷取一個使用者時,如果這個使用者在緩存中已經存在,則硬碟緩存資料源會被選中,否則會通過向雲端發起請求擷取資料,然後存儲到硬碟緩存。
這一切背後的原理是由于原始資料對于用戶端是透明的,用戶端并不關心資料是來源于記憶體、硬碟還是雲端,它需要關心的是資料可以正确地擷取到。
注:在代碼方面,出于學習目的,我通過檔案系統和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” ,我非常同意這個說法,當然了,有很多不同的方法做不同的事情(不同的實作方法),我很确定,你每天(像我一樣)會面臨很多挑戰,但是遵循這些方法,可以確定你的應用會:
- 易維護 Easy to maintain
- 易測試 Easy to tes.
- 高内聚 Very cohesive.
- 低耦合 Decoupled.
最後,我強烈推薦你去實踐一下,并且分享你的經驗。也許你會找到更好的解決方案:我們都知道,不斷提升自己是一件件非常好的事。我希望這篇文章對你有所幫助,歡迎拍磚。
## 參考資料
- Source code: https://github.com/android10/Android-CleanArchitecture
- The clean architecture by Uncle Bob
- Architecture is about Intent, not Frameworks
- Model View Presenter
- Repository Pattern by Martin Fowler
- Android Design Patterns Presentation