Android簡潔架構設計

douxingxiang發表於2015-07-03

過去幾個月,在與我Tuenti的幾位同事進行友好交流之後,其中有@pedro_g_s@flipper83(他們都是Android開發中很難對付的人),我認為是時候寫篇關於架構Android應用的文章了。這篇文章的目的是介紹近幾個月來我所想到的一些方法,以及從調查和實踐中學到的東西。

開始

我們知道編寫高質量軟體是既困難又複雜的:不僅是滿足需求方面,還要健壯、可維護、可測試,並且足夠靈活以適應增長和變化。這就是“程式碼整潔之道”的來源,並可以成為開發任何軟體應用程式的良好方法。思想很簡單:程式碼整潔之道代表構建系統的一組實踐:

  • 獨立於框架。
  • 可測試性。
  • 獨立於UI。
  • 獨立於資料庫。
  • 獨立於任何外部代理。

clean_architecture1

只限於使用圖示中的4個圓圈並不是必須的,因為這只是語義描述,你還要考慮依賴規則(the Dependency Rule):原始碼依賴只應該指向內圈,內圈不應該知道外圈的任何東西。

下面的相關詞彙可以幫助熟悉理解這個方法:

  • 實體(Entities):應用的邏輯物件。
  • 用例(Use Cases):用例編排資料流從實體的流入和流出。也叫互動器(Interactor)。
  • 介面介面卡(Interface Adapters):這些介面卡將資料轉換為用例和實體最合適的格式。表示器(Presenter)和控制器(Controller)就位於這裡。
  • 框架和驅動器(Frameworks and Drivers):這是所有細節集中的地方,UI、工具、框架等。

為了更好更深入地理解,看下這篇文章或者這個視訊

場景

我用一個簡單的場景來描述事情過程:建立一個小應用,顯示從雲端獲取的朋友或使用者列表,點選其中任意一個,將會開啟新視窗來顯示使用者的更多資訊。我提供了一個視訊來幫助理解我所說的大致過程:

嵌入視訊,需自備梯子

Android架構

架構的目標是通過將業務規則與外部世界隔離以分離關注點,這樣,才可以在不依賴外部元素的情況下測試業務規則。為了達到這個目標,我的建議是將專案拆分為3個不同的層次,每層都有自己的目標,獨立地工作。需要提及的是每層都使用自己的資料模型,這樣才可以取得依賴(你可以看到,在程式碼中需要資料對映器(data mapper)來完成資料轉換,不想在整個應用之上交叉使用自己的模型總要付出點代價)。下面是一個幫助你理解的模式:

clean_architecture_android

注意:我沒有使用任何外部庫(除了解析json資料的gson,還有測試用的junit mockito robolectri和espresso)。原因是這麼做會讓例子會更清楚。無論如何,一定不要猶豫使用那些讓生活更美好的東西,比如新增ORM來儲存磁碟資料,或者任何依賴注入框架,或者任何你熟悉的工具或庫。(記住,重新發明輪子不是一個好的實踐)。

表示層

這裡實現了與檢視和動畫有關的邏輯。它只使用一個模型-檢視-表示器(Model View Presenter, 下文稱MVP),但是你可以使用其他任何模式,如MVC或MVVM。在這裡我不會深入介紹,這裡的片段和活動只有檢視,除了UI邏輯之外沒有任何其他邏輯,這也是所有渲染髮生的地方。這層中的表示器與互動器(用例)共同在Android UI執行緒之外開啟新執行緒執行這些工作,通過回撥來處理資料,資料將在檢視中渲染。

clean_architecture_mvp

如果你想要一個使用MVP和MVVM更酷的高效Android UI例子,去看看我朋友Pedro Gómez所做的工作。

領域層

這裡的業務規則:所有邏輯都在這層。至於Android專案,你也將會看到所有的互動器(用例)實現。這層是不依賴Android的純Java模組。所有的外部元件使用介面連線到業務物件。

clean_architecture_domain

資料層

應用需要的全部資料都來自這層,資料通過一個使用倉儲模式(Repository Pattern)實現的UserRepository存取,其策略是通過一個工廠,根據特定的條件來選擇不同的資料來源。比如,當通過id獲取使用者時,如果使用者已存在於快取中,就會選取磁碟快取資料來源,不然就會查詢雲端獲取資料,之後儲存到磁碟快取。這的思想是,資料的來源對客戶端是透明的,它不關係資料到底來自記憶體、磁碟還是雲端,唯一的事實是資料將會到達,然後被獲取。

clean_architecture_data

注意:我使用檔案系統和Android首選項實現了一個簡單的磁碟快取,僅用於學習目的。再次提醒,如果已經存在某些庫可以更好地實現這些功能,你不應該重新發明輪子

錯誤處理

這個話題很容易引發討論,如果你能分享自己的解決方案會更棒。我的策略是採用回撥,因此,一旦資料倉儲有事件發生,回撥包含2個方法onResponse()和onError()。最後一個將異常封裝到稱作ErrorBundle的包裝類中:這種方法會帶來很多難處,因為只有一個回撥鏈,錯誤會一直到達表示層被渲染。程式碼可讀性略顯不佳。另外,我也可以實現一個事件匯流排系統,一旦出錯就丟擲事件,但是這個方案就像使用GOTO一樣;我認為,有時候如果你訂閱了多個事件,不仔細控制的話很容易迷失。

測試

關於測試,我傾向於根據不同層選擇多重方案:

  • 表示層:使用Android instrumentation或espresso進行整合和功能測試。
  • 領域層:JUnit和mockito進行單元測試。
  • 資料層:Robolectric(由於這層有Android依賴)和junit、mockito進行整合和單元測試。

程式碼展示

我知道你可能想知道程式碼在哪裡,對吧?這裡是github連結,你可以找到。關於目錄結構要說明的是,不同層使用模組來表示:

  • presentation:表示層的Android模組。
  • domain:無Android依賴的java模組。
  • data:所有資料存放的Android模組。
  • data-test:資料層的測試。由於使用Robolectric有些限制,我不得不在新的Java模組中使用。

結論

就像Bob大叔說過的,“架構在於目的而非框架”,我完全贊同。當然,有很多不同的做事方式(不同的實現),我確定你(像我一樣)每天都面對很多挑戰,但是使用這些技巧,你可以保證應用將會:

  • 易於維護。
  • 易於測試。
  • 非常內聚。
  • 解耦。

作為總結,我非常推薦你試試,分享你的結果和體驗,當然你也會發現更好的方法:我們都知道持續的提升總是很好充滿正能量的事情。希望這篇文章對你有用,非常歡迎你的反饋。

連結和資源

  1. 原始碼:https://github.com/android10/Android-CleanArchitecture
  2. Bob大叔的整潔架構
  3. 架構在於目的而非框架
  4. 模型 檢視 表示器
  5. Martin Fowler倉儲模式
  6. Android設計模式演示稿

相關文章