前言
這是 2014 年的舊文,稍加重新整理一下,會想重新整理這一篇是因為從前同事那裡聽到一些外包的趣事,所以把當初設計的思維和想法分享出來。當初開發的 App 已經上線了,不方便透漏太多設計的細節,文中介紹的是屬於泛用的的架構,適合大多數的 App,實作的細節則是隨專案的特性變化。
Model/View 分離
2013 年以 Android 工程師的身份進入水果公司,但就像是在遊戲中轉職一樣,沒多久就變成 iOS 工程師,在這之前,都在設計相同 App 的 Android 版的軟體架構,需要考慮的事情其實蠻多的,只是後來開始開發 iOS 版本後 (Android 版本暫緩),這些設計和想法就暫時放在腦中也沒機會整理下來。
設計時的考量主要有:(1) App 是 Internet App,在考量 UI 體驗和網路頻寬的消耗,多數資料需以某種形式 (例如:SQLite 或檔案) 儲存部分資料在行動裝置上;(2) 因此,會需要同步伺服器端和行動裝置端之間資料狀態;(3) 但行動裝置網路的穩定性不如一般網路可靠 (4G 也沒有比較好),要有足夠的自動化測試驗證正常的流程與異常的流程。
2014 年中,偶然的機會招募到數位 Android 工程師,於是 Android 版本準備開始復工,趁一些瑣碎的空檔時間,把當初想的結構給整理一下,個人對 Model 與 View 分離這件事十分堅持,讓自動化測試能夠極大化,所以基本的架構概念圖大概就如 Figure 1 所示。
Figure 1 — Android App Conceptual Architecture
Platform-independent Model
架構圖主要分成幾個區塊,綠色是 Model 的部分,所有的商業邏輯都集中在此,這個部分原則上盡可能是 platform-independent 的設計,因此以 Java SE 和 Android SDK 交集的 API 完成,在自動化測試時,完全不需要 Android 模擬器,用一般 JUnit 即可,希望用最少時間達到最大的測試涵蓋率,過去曾經統計過,核心功能部分的程式碼約是二萬八千多行,由三萬二千多行的測試程式,約一千五百個測試案例驗證功能的正確性,全部測時能在 40 秒內完成,測試涵蓋率在 95% 以上。
這邊要特別強調的是 Model,對於習慣只把資料結構當成 Model 的人來說,商業邏輯可能四散在 View、Controller (這裡的 Controller 指的是 MVC 中的 Controller) 或 Activity 中,但資料結構只是 Model 的一部分,也就是圖中 Domain Data Model 那一塊,維護資料結構物件關係的邏輯,如何回應 Use case 或 user story 所定義的系統事件 (system event),這些處理系統事件的
main controller 都屬於 Model,也就是圖中 Business Logic Managers,責任是透過 Web Service Interfaces 和伺服器溝通,然後維護資料結構物件間的關係,最後透過 DAO Interfaces 將狀態保存到行動裝置中。
Platform-dependent adapters
橘色是 platform-dependent 的實作,例如用 Android 的 SQLite API 來實作DAO,然後以 Setter Injection 或 Interface Injection 的方式,將 platform-dependent 的實作注入,為了做到這點,綠色區塊不能直接依賴 platform-dependent 的實作,因此 Web Service Interfaces 和 DAO Interfaces 兩個區塊只定義介面沒有實作,由橘色區塊中的 Web Service Implementation 和 DAO Implementation 分別實作這兩個介面,藉此將依賴關係反轉 (Dependency inversion),因此也容易把 Web Service Interfaces 和 DAO Interfaces 的 mock object 給注入,方便測試。
橘色區塊還有一個責任是扮演 Android Service 的角色,Android 能讓 App 以 Service 的方式在背景執行,以需要頻繁更新狀態的 App 類型來說,能有在背景執行的 Service 非常有用。當 Activity 被喚起,只需 bind service 就能取得所有最新的狀態。不過,設計不好,Service 是非常耗電的,若能以 GCM 喚起 App 進行單次的網路存取,也是一種用來更新狀態的一種方式,不一定得用 Service。此外,Service 與 Activity 共用同一個 thread,不能讓 Service 的初始化佔用太多時間,否則 App 啟動時,UI 會卡住無法動彈,以額外的 thread 進行初始化需要注意 multi-thread 引起的問題。
DAO Implementation 是 platform-dependent 的,所以用橘色標示沒什麼大問題,但 Web Service Implementation 卻是被標示為紫色,主要是因為以 JSON 格式傳遞資料的 Restful Web Services,是有機會只用以 Java SE 和 Android SDK 交集的 API (像是HttpURLConnection),或是是用 OkHttp,搭配 Android 上也能使用的 JSON Library (Gson 或 Jackson)來完成,也就是 Web Service Implementation 不一定是 platform-dependent 的實作,有機會能用 JUnit 來測試。
Views
淺灰色區塊即 View,主要是圖中 Android Activities & UI Flow Controls 區塊,這部分也能自動化測試,但需要 Android 模擬器或是使用實機,測試時間也較長,但 App 好不好用,這塊佔了很大的因素。此外,綠色區塊和橘色區塊的 API 會以 synchronous 的方式設計,為了不卡住 UI,Android SDK 本身有一些 Asynchronous 的輔助類別(例如AsyncTask),但我覺得還不夠好用,所以Asynchronous Supports & UI Components 會根據 App 的體驗需求客製化一些輔助類別,另外提供一些特殊的 UI 元件。
此外,Android 不允許非在 UI Thread 執行中的程式可以變更畫面,所以 Asynchronous Supports & UI Components 這區塊還要負責將不同 Thread 回來的更新通知轉到 UI Thread中。這個區塊以藍色標示,主要是因為這些元件應該設計成能跨專案共用的,當成是公司的資產,在開發新專案時就能夠使用才是長久之計。
大原則是這樣沒錯,不過準備開發的 App 會整合一個第三方的遊戲引擎,到時架構可能會有些許調整,例如在 Domain Model 或 Android Service 中多一些 Interface 與遊戲引擎的部分溝通,盡可能保護 Domain Model。最後,為了不讓不同層級的物件相互汙染,應該會用 Maven (或 Gradle) 的 module 機制將不同層級的物件歸屬到不同的 module 中,再利用 dependency 的方式限制 module 之間的可見度。希望 Android 版在需求相對明確 (已有iOS版) 的情況下,開發能夠順利些。
優缺點
最小交集
離開這專案後再回來想這個架構的優缺點,首先,為了讓自動化測試能不依賴 Android 模擬器,以 Java SE 和 Android SDK 交集的 API 完成有個缺點,要等到 Android SDK 正式支援 Java 8,才能在 Model 中開始使用 Java 8 的新特性 (像是 Lambda),不過,從 Android N 開始,已經開始支援大多數 Java 8 的特性,這缺點就沒這麼嚴重了。
跨平台共用
當初腦中確實有想過讓這 App 的核心能跨足到 PC 版,但跨平台不是最主要的考量,讓團隊成員能習慣從 domain 開始寫程式,才是這架構中 Model 與 UI 分開最主要的原因,不過後來真的開發 PC 版了,核心數萬行的程式不需重寫,想像一下,核心就像下圖中綠色的拼圖,無法單獨使用,需要其他的拼圖才能組成一個完整的 App,開發 PC 版時,則是提供藍色的拼圖整合核心的部分成為另外一個可以在 Mac 與 Windows 上執行的程式。
相依性管理
在跨平台時有一點要注意,這也是當初沒有做好的部分,當初為了開發方便,Android 相關的程式與核心的部分是同一個專案的二個模組,不像是引入第三方套件的方式引入核心到 Android 專案中,PC 版也是以 link 的方式直接將原始碼引入,但一旦核心被二個專案以原始碼形式引用時,任何一邊對核心的介面做了修改,都會導致另一邊編譯失敗,較理想的方式,應該是將核心的部分獨立出去,任何修改後建置成一個 JAR 檔並加上版本布署到內部的 repository 中,Android 與 PC 版的專案以第三方套件的方式引入,這樣便能夠以管理第三方套件版本的方式,讓雙方隨時有相容的介面可以編譯。
快速測試
從剛剛提到的統計資料可以看出來,測試程式碼與功能程式碼之間的比例約是 1.14 : 1,這同時也代表寫測試程式所花的時間幾乎和寫功能是一樣的,甚至更多,但這投資是值得的,若要以人力的方式測試二萬八千多行的功能,絕對是不可能在 40 秒內完成的,但一千五百多個測試案例卻是在每次有人簽入程式時都執行過一遍,只要 40 秒就能知道核心功能有沒有被改壞,是否安心許多呢?
最後,這設計可能的缺點之一是對於 Model 與 UI 分離這概念不熟,或是對 OO 設計不熟的人來說,有點難以上手...