本文為《Funny Vote 十年演進系列》第 2 篇(全系列共 8 篇)|查看全系列架構導覽
好一陣子忘記要更新一下 blog,沒想到好不容易建立的開發習慣也會忘記,可見怠惰真的開發的源頭啊(?),秉持著 something record 的精神,繼續保持好習慣。其實這一篇是 Funny vote 開發札記的後續,主要是記錄釋出後的幾次技術改版練習。順道一提不會教你怎麼改、怎麼用,也沒放上啥程式碼,幾乎都是我的 murmur。

根據 Google 官方的 Android Architecture 技術藍圖,我將 Funny vote 分成幾個 Git branch,各自放上想玩玩的技術架構,主要可分為:
- MVP 架構改版:補上 Unit test by Mockito、Instrument test by Espresso,以及 JaCoCo 整合進 Travis CI。
- MVP_Dagger:在 MVP 架構上使用 Dagger 2 進行依賴注入。
- MVP_Kotlin:在 MVP 架構上改用 Kotlin 重寫。
一、 MVP 架構改版
先說說最初的程式架構並沒有採用 MVP 架構,而是 MVC。原因沒甚麼特別的,就是我沒想到要用啊!人只要一急著想出產品時,都只會考慮最快做出來的方法(也不會想要寫 test case)。這並非是說 MVC 是個糟糕但快速的架構(是說到現在 MVC 到底是啥,好像也不是很確定),而是因為公司的所有 project 幾乎都是 MVC 的關係,所以最熟悉、可以開發得很快。但是也深深知道他的缺點及優點,且跟 MVP 比較之後,多多少少能理解 Google 想力推 MVP 架構的原因(其他還有一堆啦,像是 MVVM、Lifecycle、Clean Architecture)。
MVC 最主要的問題是讓 Activity 去負責所有的 Controller 邏輯任務,但是本身又負擔 View 展示與用戶介面操作交互。這樣變成 Controller 和 View 的任務混雜在一起,所以常常會看得到超過 2000 行的臃腫 Activity。根據 Clean Code 的概念,這肯定會造成 Debug 上的極大困難,因此 Google 提出了切分更乾淨的 MVP 架構:

🔹 VIEW (視圖層)
只負責 UI Init、User 操作互動,不能直接與 Model 溝通,只能接受 Presenter 的指令。
🔹 PRESENTER (業務中介層)
負責 View 與 Model 的中介互動,負責所有業務邏輯與流程決策,純 Java/Kotlin 實現,不依賴 Android SDK。
🔹 MODEL (資料數據層)
只負責處理資料庫互動(Room/SQLite)與遠端 API(Retrofit/Firebase),提供資料給 Presenter 做決策。
跟 MVC 實作上的最大差異,則是讓 Activity 只單純地去處理 View 任務,把原本在 Activity 中的 Controller 邏輯任務全部另外抽出放到 Presenter 裡。所有的交互任務則透過 View interface 和 Presenter interface 來進行,這非常方便於 Unit test。
👉 實際程式碼:Funny Vote MVP version on GitHub
在改版成 MVP 架構的同時也補上 UT,正因為有加入測試的概念,更能理解 MVP 架構的解耦優點。為了方便測試考量,在 Model, Presenter, View 交互的 interface 設計時,自然而然地會使得 code 變得更乾淨,我想這應該就是 TDD 的理念。雖然我並沒有讓 coverage rate 衝高,不過主要是因為自己懶得啥都加上測試,只主要測試核心邏輯業務。
MVP 實作開發心得
1. 一個 View 對應一個 Presenter 的多 Fragment 困境
通常一個 VIEW 對應一個 Presenter。但是如果一個 Activity 內部有兩個 Fragment View 加上 Activity 本身的部分 View(像是 Funny Vote 創建投票頁面):先不說我當初 UI 設計是怎麼想的,最初的思路認為應該讓一個 CreateVotePresenter 去 handle setting fragment view、option fragment view 以及 activity 的 title and image view。

會這樣想,只是單純地認為 Activity 跟 Fragment View 之間的互動,可以很簡單地在一個 Presenter 中控制,可以少掉很多業務上的 Callback,可以讓 code 變得易讀。但是事情果然不是小弟想的那麼簡單,在經過實作後,發現就會發生一個 Presenter 的 code 太多,且太多 View 要一起 ready。這在一個人開發時還好,但如果是多人開發時,對於協調 interface 設計工作就會有極大的負擔。
當然這並非是絕對的原則,主要看 widget 的內容量來定義 MVP 會比較好。至於 Callback 太多這個問題,當然不是只有我想到,各方大神早已有解法,也就是 RxJava。該有的 callback 是少不了,但是可以讓 callback 好創造、好管理,這就是好框架。
2. 嚴禁在 Presenter 出現 Android SDK 相依
Presenter 不應該有任何相關 Android 的 Object and Method(例如 Context、View 等),這沒啥好質疑的,只是我想來自我提醒而已,不然以後寫 UT 只會想哭。Presenter 也不應該傳入 Adapter 裡。
3. Model 層的資料來源隔離與樣板模式
Model 設計上,如果常常會遇到後端還未 ready 時,常常需要自己先在 local fake data,當後端完成後再重新接上。這種情況下非常適合使用樣板模式,搭配上 Injection 和 build variant 可以簡單區分出何時用何種 content,超級舒爽。在這個概念之上,同時方便測試的目的,Dagger 的依賴注入框架也因此被提出來。
4. 測試框架規範與命名學問
測試框架在沒有特殊需求下,則依據 Google 建議的 Mockito 做 UT、Espresso 做 Instrument test。目前感覺就是寫而已,也沒用過別的測試框架。硬要說的話就是 test method 的命名怎麼樣比較好(參考大師 Teddy Chen 說法):一直以來都是 Presenter, View UT 用「想測啥功能」作為命名規則,Model UT 則多採用「預期行為加上測試狀態」作為命名規則,Instrument test 則以 Given-When-Then 為命名基準。老實說這樣好不好我也不適很清楚,個人覺得有規則就好。
5. CI/CD 自動化與 JaCoCo 代碼覆蓋率
CI 這個以前完全沒有摸過,但是聽說 Travis 超簡單,所以我就給他蓋下去了,老實說爽感十足,看他在那邊自動化,我在看 YouTube 真的方便。基本功能只驗證能不能 build 過,如果有寫測試的話,則會順手跑完。可是卻發現用 Android Studio 都內建顯示 coverage rate 範圍,結果 Travis 沒有順手提供這功能不是太可惜了嗎,因此也整入 JaCoCo,測試完後自動 gen report(參考大神整合好的範例)。
“You need to provide a way of accessing the member variables so you can pass in a mock (the most common ways would be a setter method or a constructor which takes a parameter).”
“If your code doesn’t provide a way of doing this, it’s incorrectly factored for TDD (Test Driven Development).”
— 查資料時節錄的名言
二、 MVP with Dagger 2
在上一章節談到兩個可以在 MVP 架構上使用的框架,一個是解決大量 Callback 以及流程化的 RxJava,另一個是解決大量 new 依賴且方便 Injection 管理的 Dagger 2。而這個章節則是談引入 Dagger 2 的心得。你說為何沒有一起引入 RxJava,難道那些問題不用一起解決嗎?我絕對不會說因為看起來太簡單了,所以我懶得用,而是因為 Kotlin 就直接把這塊整合得更好。雖然我知道這有點雞腿比懶覺的問題,但是因為這之後我打算直接寫 Kotlin 版本,所以就忽略了。
根據上面一堆屁話,直接開始 Dagger 整合。通常一開始查找資料都會聽到「MVP + Retrofit + RxJava + Dagger」整個超舒爽,但是接下來就會看到說 Dagger 的學習難度比較高,大部分用在大型協作專案比較適合。恩~老實說我覺得還行啊,以前有玩過,原本預估個 8 小時應該可以搞定,最後花了 15 小時,是比較麻煩啦,主要是 Dagger 的錯誤提示訊息有點難懂。
這種東西就是初始化麻煩,但是後來用起來整個舒爽!最明顯的就是 MVP 版本時有一個 Injection.java,這東西可以根據放的資料夾以及 build variant 去提供 debug version, prod version 不一樣的實作注入;但是 Dagger 就不用了啊!等等,好像只是移到 Module 了。不要小看這個 Module, Component,一堆奇妙的 new 就通通不見了(解耦的性質就出現了),也不用管理物件的生命週期(專注業務邏輯),在管理上也因為有好好的分類,開發效率也會提高(清晰的 code),撰寫 UT 也會方便得多。
👉 實際程式碼:Funny Vote MVP with Dagger2 version on GitHub
Dagger 實作開發心得
- 理解導入框架的代價:使用任何新框架開發都請先理解為何要導入。以 Dagger 來說,主要是解決依賴嚴重的問題,接著就會引導到方便測試的好處。但是導入框架都會有它應該付出的代價,個人覺得最煩的是初始化流程,接著為了讓 Module, Component 整個流程能達到注入的效果,大概會多出兩倍的接口設計,真的一般小型開發的 solo 玩家們,可以好好思考需要嗎?
- 樣板代碼簡化之道:由於多出大量 Module, Component 的物件和接口設計,這就是一個麻煩的問題,寫的程式碼越多就越有機會出 BUG。那麼問題來了,就要思考有沒有辦法簡化。因為大部分的 Module, Component 雖然一堆接口要設計,但大都是模板化的代碼,果然 Google 大神提出了 dagger.android 的工具,用 Annotation 去省略模板代碼,讓 Compiler 去自動生成程式碼。
- 模組解耦後,務必寫測試:舒爽!
“Dependency injection frameworks can simplify the code you write and provide an adaptive environment that’s useful for testing and other configuration changes.”
“If you intend to use a dependency injection framework in your app, consider using Dagger 2. Dagger does not use reflection to scan your app’s code. Dagger’s static, compile-time implementation means that it can be used in Android apps without needless runtime cost or memory usage.”
— Google 官方文檔對 Dagger 2 的評價
以上是本次小弟的感想,我真的懶得貼 CODE,各位真的想看去 GitHub 吧,通通都放在那。至於 Kotlin 版本我則放到下一篇,對小弟來說 Kotlin 算是真的完全沒用過,所以側重點會多放在怎麼用,跟本篇性質不太相同,所以新開文章!
《Funny Vote 十年演進系列》章節導航

發佈留言