2017/12/1

使用遠端的 AVD 進行測試

測試上遇到的困擾

測試一直是軟體開發中重要的一環,在「軟體測試經驗分享」中,提到了一些測試相關的概念。Android 的開發裡,AVD 則是測試不可或缺的一部份。透過 AVD 可以不用準備大量的實體設備,就可以讓所開發的 App 在不同的環境中檢視其運作的情況。

使用 Notebook 進行 Android 開發的人,通常都會遇到一個困擾是:Notebook 的磁碟空間大多不是很充裕。在開發 Android App 時,常常要使用多個 AVD,來測試 App 在不同的環境下都可以正常地運作,畢竟 Android 的平台破碎化已經是一種特色了。

然而在空間有限的情況下,建一個 AVD 就要上 Giga 的空間,這還不包含不同版本的 Image 檔。每一個版本還有分 32bit、64 bit,有 Google API、沒有 Google API,再加上 TV 和 Wear 這二種設備種類。林林總總的 Image 加起來,配上 AVD 可能要產生出不同畫面尺寸的組合,對於有限的磁碟空間來說更是捉襟見肘。

如果要擴充 Notebook 的儲存空間,除了價格上的因素以外,往往還會受限於 Notebook 硬體架構上的侷限性。就這個缺點來說,PC 相對是一個比較好的選項。但是改為使用 PC 開發,就會失去了原本 Notebook 俱備的機動性。想要二者優點兼俱,替代性的選項是在二台機器間進行 Source Code 同步,需要機動性就在 Notebook 上開發,需要空間時就轉移到 PC 上開發。

這個選項對大型公司而言,是一個再直接不過的選項,因為公司內通常都會有一定程度的版本控管系統在協助,檔案移轉的負擔比較小,甚至更豪華的有專用 Lab 環境。反過來說,不在這樣體制內的人,只能苦命地手動將 Source Code 反覆傳來傳去,費工又容易出錯。

對於這樣的族群來說,需要將選項的內容再進化,也就是開發仍在 Notebook 上保持機動性,同時選擇關鍵的環境來建立 AVD,做基本的確認。而 PC 上則是可以廣泛地建立不同選項的 AVD,要進行測試時就將二個環境結合起來,將 Notebook 建置好的結果傳送到 PC 上執行測試。

所以現在的問題是,這樣的要求做得到嗎?要怎麼做?

可行的方案

首先,先來了解負責管理受測裝置的工具 - ADB 的運作原理,這個工具包含了三個部份 Client、Daemon、Server:
  • Client: 負責接收我們輸入的指令,並且傳送給所連接的 Server,一般就是我們在 Command-line 使用的 adb 指令。
  • Daemon: 駐紮在行動裝置上,可以是實體設備或是 AVD,負責在裝置上執行由 Server 傳來的指令。
  • Server: 在電腦上執行,負責管理 Client 與 Daemon 之間的訊息傳遞。


從上圖來看,可行的方案有三個:第一個是透過 Server 轉送訊息給 Daemon,第二個是讓 Client 連接到遠端的 Server,第三個方案則是讓 Server 連到遠端的 Daemon。

第一個方案是使用 adb forward 的指令,讓 Daemon 可以收到所處 PC 向 127.0.0.1 發出的訊息,並轉給 AVD 指定的 Port。這個動作最直接的效果,是讓所在的 PC 可以不用透過 Console,直接與 AVD 中的 Service 進行溝通。但似乎與跨機器無關,所以先略過。

第二個方案要達成不難,在 adb 的指令中有參數分別是 -H-P 可以指定 Server 的 Address 及 Port。例如以下的指令可以列出 192.168.1.1 電腦上,ADB Server 所管理的裝置清單。

adb devices -H 192.168.1.1 -P 5038

但有一個很關鍵的問題是,如何設定讓 Android Studio 使用遠端的 ADB Server 來進行部署及測試?IDE 和 Gradle 我都沒有找到答案,也許是這樣的選項還不存在吧!會有這個結果,最主要的問題應該是卡在如何讓遠端的 ADB Server 拿到建置好的結果,以傳送進 AVD 內。

不過,倒是可以使用以下示範的指令來啟動遠端ADB Server 的測試,只不過連部署的工作都要在建置完成後,用 adb 自己動手。

adb shell am instrument -w <test_package_name>/<runner_class> -H 192.168.1.1 -P 5038

方案三,也是可以用adb 的指令來達成,以下的指令可以讓 ABD Server 連接到 192.168.1.1 電腦上的 AVD。

adb connect 192.168.1.1:3333

連上之後,就可以在 IDE 的「Select Deployment Target」的視窗中,看到遠端的 AVD 被列在「Connected Devices」清單中。接下來的動作就跟過往一樣,選擇遠端 AVD 的項目後,按下【OK】,等建置完成就可以看到 App 出現在遠端 AVD 的畫面裡。

這個方案比第二個更具優勢的是,可以直接在 IDE 或是 Gradle 中就完成部署及測試,維持原本的開發經驗一氣呵成,不用再另外做設定。

需注意的技術細節

在方案二或方案三的指令中,如果直接進行連線,應該會遇到問題、無法完成連線。主要的原因可以看一下圖示:


ADB Server 聆聽的 Address 是 127.0.0.1,而外部傳入的 Port 是在真實的 IP 上,也就是說訊息並沒有傳到 ADB Server 所在的 Port 上。為了要解決這樣的情況就必須要把真實 IP 接到的訊息 Forward 到 127.0.0.1 所在的 Port。

以 Windows 為例,內建有 netsh 指令來進行這項工作。以下的指令就是把真實 IP 在 5038 上收到的訊息,轉送到 ADB 預設的 5037 Port 上:

netsh interface port proxy add v4tov4 listenport=5038 connectaddress=127.0.0.1 connectport=5037

這裡有個小細節要注意,在設定 ADB Server 的聆聽 Port 轉送時,不能使用同一個 5037,會導致 ADB Server 無法啟動。

同時,AVD 的 Port 是成對配置的,ADB 管理的範圍是由 5554 到 5585,每二個號碼分配給一個 AVD,偶數的號碼是給 Console 連線用,奇數才是保留給 ADB。第一個 AVD 分配到的會是 5554,而 ADB 使用的則為 5555。以下圖的 AVD 為例,顯示的是 5558,但要進行遠端連線的 Port 應該加一號為 5559。



AVD 的 Port 也不要佔用到 5554 到 5585 的範圍,否則 ADB 會認為 Port 被分配走,而跳過改取下一組。

這個設定是有持績性的,不會因為重新開機而消失。而執行這個指令需要有 Administrator 的權限,所以可以一次就設定好清單,畢竟範圍也就只有從 5555 到 5585。如此一來,不論 AVD 被分配到哪一個 Port 上,都可以被遠端連接上。

所能達到的效果

最後總結一下以這種方式進行開發的好處:
  • 節省開發環境的磁碟空間,並且可以更彈性地選擇測試設備的規格,不用被開發的機器所限制。
  • 有效利用閒置資源、增加工作效率。用 Notebook 開發的人,另外擁有 PC 是很常見的情況,只是很有可能在開發時是閒置的。而開發時最耗資源的都是測試,所以如果可以把耗資源的部份轉移到效能相對較好的 PC 上,可以有效地提高效率。
  • 減少空轉與等待的情況。延續上一點,耗資源的工作被轉出去,開發的機器就可以接續進行開發的工作,或是放鬆休閒一下,上上網、看個影片,等待測試完成。

額外的異常狀況

在嘗試以上解決方案的過程中,遇到了一些異常的情況,順便記錄一下解決的方法。

首先,用 Windows 做為遠端測試設備時,除了要設定 Port Forward 之外,還要確認 Firewall 是否有阻擋訊息的交換。如果 Firewall 是在開啟的狀態之下,有很大的機會訊息是無法傳遞的。此時,就要調整 Firewall 的設定,增加適合的允許規則。

在使用最新版的 Android SDK Tools 時,與先前的版本不同的是,不再提供像 Android Studio 所附的 GUI 工具,完全是由 Command-line 執行工作。不過,也還好,問題不大,就是打字多了點。

有問題的是 avdmanager 在參數被大幅減化之下,產生出來的 AVD 似乎與 Android Studio 建立出來的有差距。再來是 sdkmanager 可下載的 package 中,似乎也沒看到 AVD Skin 這個選項。結果用 avdmanagerdevice 參數做出來的 AVD,縮放都不太正常,畫面大到不合理,只能看到 AVD Desktop 的左上角。而用來啟動 AVD 的 emulator 看似可以調整這個問題的參數,在新版也早就被廢棄了。

為了這個問題,查了好久,試了很多參數,都沒有辦法解決。根據網路上的說法,似乎是個 Bug,而且有一段時間了、歷經了好幾個版本都沒有修正,真是好樣的,Google。

原本想要放棄,改為安裝 Android Studio 算了。只是突然靈機一閃,把其他機器產生好的 AVD 移過來,有缺 Skins 這個目錄的也要一併把 Skins 移到 SDK 下。調整一下 ini 裡路徑的設定,居然就可以正常地顯示了,目前看來堪用中。








2017/11/14

FluxJava 新增 RxJava2 的支援功能

FluxJava 最初的設計就是以 Add-on 的方式來提供對於 RxJava 的支援,所以這次增加 RxJava2 的部份也依照相同的模式,在 Project 中加上了 fluxjava-rx2 的 Module。新的 Module 功能上與 fluxjava-rx 大致上相同,只是原本以 RxJava 規格運作的部份,改為使用 RxJava2。

由於 RxJava 與 RxJava2 不太有機會共存在同一個 Module 裡,所以 fluxjava-rx2 沿用了 fluxjava-rx 的 Package 名稱,在使用上這二個 Add-on 必須要擇一引用。不過也帶來了一個額外的好處,如果想要由 fluxjava-rx 昇級到 fluxjava-rx2 時,只要修改成 RxJava2 的呼叫規格,不用再特別調整 Import 的內容。

配合 RxJava2 的更新,按照慣例增加了一篇文章「FluxJava 與 RxJava2 結合的使用示範」做為使用上的參考。

在 fluxjava-rx2 中與 fluxjava-rx 最大的差異,主要是因應 RxJava2 把原本的 Observable 分成了有背壓版本的 Flowable 與沒有背壓版本的 Observable。因此 RxBus 與 RxStore 中都分別再提供了 toFlowable 的 Method 來取得 Flowable,不過 Observable 本來就可以再轉換為 Flowable,此處的功能只是為了增加便利性、簡化程式碼之用。

同時,利用這篇文章補充說明一下一些使用上的技術細節。在 RxStore 中使用的是沒有背壓版本的 Observable 來接收外部傳來的訊息,只是接收到之後就會被分派到不同的 Thread 上去處理後續的工作,所以在這個部份是不大有機會遇上 MissingBackpressureException 問題的。但是,這並不代表背壓所造成的情況就不存在了,只是瓶頸移到了 ThreadPool 的承受能力或是執行的環境可以產生 Thread 的數量上。

就算是使用有背壓功能的 Flowable,也不代表就可以高枕無憂了。說穿了,背壓本身不是什麼神奇的黑科技,原理上只是在上下游中間加了個水池,讓下游有喘息的空間。水池畢竟還是有一定的物理限制,沒有控制好,依舊會讓水池承載不下而出現錯誤。

因此,不論使用哪一種方法,前端發送的數量仍然應該要被謹慎地控制,避免海量的訊息把接收端給淹沒了。像是把 RecyclerView 滾動時產生位移的 Event 毫無選擇地往 RxStore 送。

在發送端就要篩選訊息,除了要減少 MissingBackpressureException 可能會出現的機會外,還有一個目的是要節省執行成本。當訊息數量大到無法處理時,能做的只有挑選值得處理的部份來進行。當挑選的工作被移到發送後,RxJava 的傳送機制表面上看起來就是簡單地轉了一手,但是如果去追蹤其程式碼,就可以發現其實底下做了不少的工作。而每一次傳送都要執行這些內容,可以想見在數量到達一定的程度之後,就會顯現出可觀的效能差距。

由於 Observable 在定義上所形成的限制使然,同一個發送源無法把的訊息分配至不同的 Thread 上送出。RxStore 為了要讓每個資料處理要求可以獨立、同步地進行,所以才會在接收到訊息後,以不同的 Thread 進行後續的工作。

在 RxStore 裡提供了一個 getExecutor Method,可以使用 ThreadPool 來做為背壓的替代方案,但是並沒有像背壓一樣有可以控制上游的功能。在實作 getExecutor 時要注意,不要直接在回傳時 New 一個 ThreadPool 的 Instance。因為 getExecutor 是在每一次收到訊息後呼叫一次。以上的做法,也等同於每一次收到訊息就拿到一個 ThreadPool 的 Instance,每一個 ThreadPool 都只會產生一個 Thread,這就失去了使用 ThreadPool 的用意。

最容易出現以上問題的情況是使用 Executors 來取得 ThreadPool,一般的情況下很容易忽略 Executors 其實是每次呼叫就產生一個 Instance。所以當以 return Executors.newFixedThreadPool(poolSize); 的方式回傳 getExecutor 時,一開始也許顯示不出問題而被略過,但是一但訊息爆量後,就會因為產生 Thread 的數量到達上限而中止運作。

以上,是這次補充的內容,讓使用 FluxJava 的朋友做為參考。






2017/11/13

FluxJava 與 RxJava2 結合的使用示範

這篇是「FluxJava: 給 Java 使用的 Flux 函式庫」的延續,會透過建構一個示範的 Todo App 的過程來說明如何使用 FluxJava,所有示範的原始碼都可以在 Github 上找到。

大部份的內容其實都已經寫在另一篇中,但考量到有一些性急的朋友對於要開另外一篇文章,來看與想找的主題無關的內容會感到不耐,所以在這篇文章中還是會呈現完整的步驟及相關的內容。

Flux 簡介

為了方便不熟悉 Flux 的讀者,一開始會先簡短地說明這個架構。以下是借用 Facebook 在 Flux 官網上的原圖:


從圖上可以看到所有箭頭都是單向的,且形成一個封閉的循環。這一個循環代表的是資料在 Flux 架構中流動的過程,整個流程以 Dispatcher 為集散中心。

Action 大多是由與使用者互動的畫面元件所發起,在透過某種 Creator 的方法產生之後被送入 Dispatcher。此時,已經有跟 Dispatcher 註冊過的 Store 都會被呼叫,並且經由預先在 Store 上定義好的 Method 接收到 Action。Store 會在這個接收到 Action 的 Method 中,將資料的狀態依照 Action 的內容來進行調整。

前端的畫面元件或是負責控制畫面的元件,會收到由 Store 在處理完資料後所送出的異動事件,異動的事件就是用來代表在資料層的資料狀態已經不一樣了。這些前端元件在自行訂義的事件處理方法中聆聽、截收這些事件,並且在事件收到後由 Store 獲取最新的資料狀態。最後前端元件觸發自己內部的更新畫面程序,讓畫面上所有階層子元件都能夠反應新的資料狀態,如此完成一個資料流動的循環。

以上是一個很簡單的說明,如果需要了解更進一步的內容,可以自行上網搜尋,現在網路上應該已經有為數不少的文章可以參考。

以 BDD 來做為需求的開端

為了能夠更清楚的說明程式碼的細節,所以文章中會依照 BDD 的概念來逐步解說程式碼。所以首先是要先列出需求:
  • 顯示 Todo 清單
  • 在不同使用者間切換 Todo 清單
  • 新增 Todo
  • 關閉/重啟 Todo
接著下來就要把需求轉更明確的敘述內容,因為只是示範,所列僅做出一個 Story 做為目標:
Story: Manage todo items

Narrative:
As a user
I want to manage todo items
So I can track something to be done


Scenario 1: Add a todo
When I tap the add menu on main activity
Then I see the add todo screen

When I input todo detail and press ADD button
Then I see a new entry in list


Scenario 2: Add a todo, but cancel the action
When I tap the add menu on main activity
 And I press cancel button
Then Nothing happen


Scenario 3: Switch user
When I select a different user
Then I see the list changed


Scenario 4: Mark a todo as done
When I mark a todo as done
Then I see the todo has a check mark and strike through on title
也因為只是示範用,Story 的內容並沒有很嚴謹,而示範所使用的文字雖然是英文,但在實際的案例上用自己習慣的文字即可。

規劃測試的方略

既然是採用 BDD 來做示範,當然在程式撰寫的過程中會希望能夠有適切的工具的輔助,畢竟工欲善其事、必先利其器。所以在撰寫測試程式碼時,不使用 Android 範本中的 JUnit,改為使用在「使用 Android Studio 開發 Web 程式 - 測試」提到的 Spock Framework,並且全部以 Groovy 來撰寫。因為 Spock Framework 內建了支援 BDD 的功能,把之前做好的 Story 轉成測試程式碼的工作也會簡化很多。

接下來要決定如何在 Android 專案中定位與分配測試程式碼。Espresso 是 Android 開發環境中內建的,使用 Espresso 來開發測試程式還是有一定的必要性。只不過 Espresso 必須要跑在實機或模擬的環境上,執行效率問題是無法被忽視的一個因素,而且 androidTest 下的程式碼其執行結果也沒有辦法在 Android Studio 中檢視 Code Coverage 的狀態,所以用 Espresso 撰寫的測試程式並不適合用來做為 Unit Test。

再加上新版的 Android Studio 提供了錄製測試步驟的功能,最後會被轉成 Espresso 的程式碼。所以看起來 Espresso 比較適合用來做為開發程流後段的一些測試工作,像是 UAT、壓力測試、穩定度測試。依據這樣的定位,之前寫好的 Story 會在 Espresso 上轉成測試程式碼,來驗證程式的功能是否有達到 Story 描述的內容。

單元測試的部份沒有疑問地應該是寫在 Android 專案範本所提供的 test 的路徑之下,要解決的是 Android 元件如何在 JVM 的環境中執行測試。大部份人目前的選擇應該都會是 Robolectric,只不過測試程式碼要使用 Spock 來開發,所以這二個套件必須要做個整合。RoboSpock 就是提供此一解決方案的套件,可以讓 Robolectric 在基於 Spock 所開發的 Class 中可以被直接使用。

使用 Robolectric 雖然能夠對 Android 元件在 JVM 中進行測試,但畢竟這類的元件相互之間的藕合性還是有點高,尤其是提供畫面的元件。所以這個部分在歸類上我定位成 Integration Test,但在資料的供給上,拜 Flux 架構之賜,可以依照情境來進行代換,只測試 Android 元件與元件之間的整合度,這個部份在接下來的內容會進行說明。附帶一提,有關測試上的一些想法我有寫成一篇文章,可以參考:「軟體測試經驗分享」。

以下列出本次使用的測試套件清單:
  • Groovy
  • Spock Framework 
  • RoboSpock
  • Espresso
設定 Spock 與 Espresso、Robolectric 時會有一些細節需要注意,相關的說明請參考「設定 build.gradle 來用 Spock 對 Android 元件進行測試」。最後的 build.gradle 設定結果,可以在 Github 上的檔案內容中看到。

建立畫面配置

在產生完 Android 專案空殼後,首先修改 MainActivity 的內容。在 MainActivity 畫面中加上 RecyclerView 及 Spinner 來顯示 Todo 清單以及提供切換使用者的功能。Layout 的配置顯示如下:

開發 UAT

原本範本中預設產生的 androidTest/java 的路徑可以刪除,另外要在 androidTest 之下增加一個 groovy 的目錄,如果 build.gradle 有設定正確,在 groovy 目錄上應該會出現代表測試程式碼的底色。因為目前只有一個 Story 所以在 groovy 路徑下配對的 Package 中增加一個 ManageTodoStory.groovy 的檔案。

在這裡就可以顯現 Spock 所帶來的優勢,把之前的 Story 內容轉成以下的程式碼,與原本的 Story 比對並沒有太大的差距。

如果 Story 是用中文寫成的,以上的套用方式還是適用的。有關 Spock 的使用方式在這裡就不詳細地說明,各位可以自行上網搜尋,或是參考我之前寫的這一篇這一篇有關 Spock 的文章。接著就是把程式碼填入,完成後的內容如下所示:

以上程式碼中使用到的資源當然是要在撰寫之前就事件準備好,否則出現錯誤的訊息。完成後先執行一次測試,當然結果都是失敗的,接下來就可以依照需求來逐項開發功能。

了解 RxBus

除了原本 IFluxBus 所定義的 Method 以外,為了善用 RxJava 所帶來的優勢,RxBus 提供一個 toObservable 的 Method,利用這個 Method 所傳回的 Object,可以進行所有 RxJava 所提供的功能。

同時,在向 Observable 訂閱時會取得 Subscription,RxBus 可以協助管理 Subscription。在 Subscriber 向 RxBus 取消註冊時,一併更新所屬的 Subscription 狀態,以避免持續地收到通知。

如果是透過 registerunregister 來註冊 Subscriber,則不需要特別處理 Subscription 的問題。但如果是由先前所提到 toObservable 的 Method 來訂閱,則另外要呼叫 addSubscriptionremoveSubscription 來將 Subscription 列入與移除 RxBus 的管理機制。

因應 RxJava2 新的設計,RxBus 也提供了 toFlowable 的 Method,用來取得有背壓版本的 Observable。當然,要透過 toObservable 再轉換為 Flowable 也行,toFlowable 就只是做為提供一個便利、縮減程式碼的用途。

準備 Model

這裡的 Model 是二個 POJO,分別用來代表一筆 User 和 Todo 的資料內容。因為這部分並不是示範的重點,所以檔案的內容請自行參考 User.javaTodo.java

定義常數

常數主要的作用是以不同的數值來區分不同的資料種類,以及每一個資料種類因應需求所必須提供的功能。如同以下所展示的程式碼內容:

在需求中提到需要處理二種類型的資料,所以就分別定義了 DATA_USERDATA_TODO 來代表使用者及 Todo。以 User 的需求來看,在畫面上只會有載入資料的要求,以提供切換使用者的功能,所以 User 的動作只定義了 USER_LOAD。而 Todo 的需求就比較複雜,除了載入資料以外,還要可以新增、關閉 Todo。所以目前定義 TODO_LOADTODO_ADDTODO_CLOSE 等三個常數。

這些常數接下來會被用在 StoreMap 的鍵值及 Action 的 Type。在 FluxJava 中並沒有限定只能使用數值型別來做為鍵值,可以根據每個專案的特性來設定,可以是字串、型別或是同一個型別不同的 Instance。

撰寫 Action 及 Store

UserAction 和 TodoAction 都是很直接地繼承自 FluxAction。其中比較特別是:考量到一次可能會要處理多筆資料,所以在 Data 屬性的泛型上使用 List 來做為承載資料的基礎。這二個 Class 的內容請直接連上 Github 的 UserAction.javaTodoAction.java 二個檔案查詢。

Store 可以繼承 FluxJava 內建的 RxStore,在 RxStore 中  registerunregister 是提供給前端的畫面元件,做為向 Store 登記要接收到資料異動事件之用。與 RxBus 相同,RxStore 額外提供一個 toObservable,如果想要取得更多在使用 RxJava 上的彈,可以改為使用 toObservable

當外部所有的呼叫都是使用 toObservable 來進行訂閱,則不會使用到 IRxDataChange 的 Interface,這個介面是透過 register 訂閱時才會需要實作。

附帶一提,與 RxBus 相同,RxStore 也有提供 toFlowable 用來取得有背壓版本的 Observable,使用的方式與 RxBus 相同。

Tag 則是考量到同一個 Store 有可能要產生多個 Instance 來服務不同的畫面元件,所以仿照 Android 元件的方式,用 Tag 來識別不同的 Instance。像是在同一個畫面中,可能會因為需求的關係,要使用不同條件所產生的清單來呈現圖表。這時就有必要使用二個不同的 Instance 來提供資料,否則會造成畫面上資料的混亂。

至於 getItemfindItemgetCount 都是很基本在呈現資料內容時需要使用到的功能。其中 getItem 之所以限定一次只取得一筆資料,而不是以 List 的方式傳回,主要是為了符合 Flux 單向資料流的精神。如果 getItem 傳回的是 List,前端很有可能意外地異動了清單的內容,根據 Java 的特性,這樣的異動結果也會反應在 Store 所提供的資訊上。也就等於資料的清單在 Store 以外,也有機會被異動,這就違反了 Flux 在設計上所想要達成的資料流動過程。

當然,就算是只提供一項資料,前端也許改不了整個清單,但還是可以修改所收到的這單一項目,其結果一樣會反應回 Store 的內部。所以在示範的程式碼中,在 getItem 所傳回的是一個全新的 Instance。

在 RxStore 中有一個關鍵的 Method 是要覆寫的,那就是 onAction,是用來接收前端所推送出來的 Action。而 getActionType 可以用來指定特定的 Action 型別,避免收到不相關的 Action 通知。以 UserStore 為例,會有以下的內容:

可以看到之前定義的常數在這裡派上用場了,利用 Action 的 Type 可以區分出前端所接收到的指令。在這個 Demo 中,Store 的定位只是用來管理清單,清單的資料會由 ActionCreator 傳入,所以可以看到程式碼中只是做很簡單的載入工作,載入完即發出資料異動的事件。這個事件是定義在 Store 內部,每個 Store 都有定義自己的 Event,以便讓前端元件判別與過濾所想收到的 Event 種類。

在以上的 Method 程式碼中,使用了 RxStore 所提供的功能,在接收到 Action 的當下是以背景的 Thread 在執行,避免因為過長的資料處理時間導至前端畫面凍結。Method 的參數則是用以過濾 Action,讓指定的 Action 型別在 Bus 中被傳遞時才呼叫 Mehtod,減少程式碼判斷上的負擔。如果是同一個 Store 有多個 Instace 同時存在,在接收到的 Action 中可以加入 Tag 的資訊,以便讓 Store 判別目前傳入的 Action 是否為針對自己所發出來的。

而因為需求的關係,同樣的 Method 在 TodoStore 中就相對地複雜了一點:

主要是多了二種資料處理的要求:在新增時,前端會把新增的內容傳入,所以這裡很簡單地把收到的項目加入清單之中,就可以通知前端更新資料。至於在關閉 Todo 的部份,由於之前提到 Store 在 getItem 回傳的都是全新的 Instance,所以要先進行比對找出資料在清單中的位置,因為是示範的緣故,很單純地只寫了個迴圈來比對。找到了對應的位置後,直接以新的內容取代原本清單中的項目,再通知前端更新畫面。

如此,Action 與 Store 的撰寫工作就算完成了。同樣地,在這個階段的最後,執行寫好的測試程式來確認目前為止的工作成果。

撰寫 ActionHelper

FluxJava 已經內建了一個負責 ActionCreator 的 Class,這個 ActionCreator 使用 ActionHelper 來注入自訂的程式邏輯。可自訂的內容分為二個部份,第一個是決定如何建立 Action 的執行個體,第二個是協助處理資料格式的轉換。

以下是第一個部份的示範程式碼:

內容的重點就是依照先前定義好的常數來指定所屬的 Action 型別。

第二個部分就會有比較多的工作需要完成:

根據 Flux 文件的說明,ActionCreator 在建立 Action 的時候是呼叫外部 API 取得資料的切入點。所以 ActionHelper 提供了一個 wrapData 來讓使用 FluxJava 的程式有機會在此時取得外部的資料。在以上的程式中,還另外示範了另一種 wrapData 可能的用途。由於在前端會接收到的資訊有可能有多種變化,像是在示範中,要求載入 User 時只需要一個數值、在載入 Todo 時則要額外告知此時選擇的 User、在新增或修改 Todo 時則是要把修改的結果傳入。這時 wrapData 就可以適時地把這些不同型式的資訊轉成 Store 要的內容放在 Action 中,讓 Store 做後續的處理。

如果想要使用自訂的 ActionCreator,可以在初始化 FluxContext 時將自訂的 ActionCreator Instance 傳入,只是這個自訂的 ActionCreator 要繼承自內建的 ActionCreator,以覆寫原本的 Method 來達到自訂的效果。

組合元件

這次示範中,Flux 的架構橫跨整個 App 的生命週期。所以最合理的切入位置是自訂的 Application,這裡增加了一個名為 AppConfig 的 Class 做為初始化 Flux 架構的進入點,同時修改 AndroidManifest.xml 讓 AppConfig 可以在 App 啟動時被呼叫。

在 AppConfig 內增加一個 setupFlux 的 Method,內容如下:

重點工作是把之前步驟中準備好的 Bus、ActionHelper、StoreMap 傳入 FluxContext 的 Builder 之中,並且透過 Builder 建立 FluxContext 的 Instance。截至目前為止,後端準備的工作算是完成了,在目錄的結構上各位應該可以看出來,我把以上的 Class 都歸類在 Domain 的範疇之中。

撰寫 Adapter

Adapter 是用來供給 Spinner 及 RecyclerView 資料的 Class,同時在這次的示範中也是與 FluxJava 介接的關鍵角色,代表的是在 Flux 流程圖中的 View。在 MainActivity 中 Spinner 是用來顯示 User 清單,而 RecyclerView 是用來顯示 Todo 清單,所以各自對應的 Adapter 分別是 UserAdapter 及 TodoAdapter。

雖然這二個 Adapter 繼承自不同的 Base Class,但是都需要提供 Item 的 Layout 以便展示資料。所以先產生 item_user.xmlitem_todo.xml 二個檔。

準備好了 Item 的 Layout 就可以進行 Adapter 的撰寫工作,以下是 UserAdapter 的完整內容:

在 UserAdapter 的 Constructor 中,使用 FluxContext 來取得 Store 的 Instance。使用的第一個參數就是之前在常數定義好的 USER_DATA,第二參數的 Tag 因為本次示範沒有使用到所以傳入 Null。最後一個參數是把 Adapter 本身的 Instance 傳入,FluxContext 會把傳入的 Instance 註冊到 Store 中。當然,如果要在取回 Store 後再自行註冊也是可以的。

之後部份就是 Adapter 的基本應用,需要提供資料有關的資訊時,則是透過 Store 來取得。

在 Adapter 的 Constructor 中可以看到以 RxJava 的方式向 Store 進行訂閱的程序,可以用來接收資料異動的事件。傳入的 Action 型別參數是用來限定要收到的事件種類,被呼叫後的工作也很簡單,就是轉通知 Spinner 重刷畫面。由於是要更新畫面上的資訊,所以要回到 UI Thread 來執行,observeOn 被指定為 MainThread。如果同一個 Store 同時有多個 Instance 存在,和 Store 的 onAction 一樣,可以在 Event 中加入 Tag 的資訊,以減少無用的重刷頻繁地出現。

最後則是一個用來釋放 Reference 的接口,主要之目的是避免 Memory Leak 的問題,大部份都是在 Activity 卸載時呼叫。

以下是另外一個 Adapter - TodoAdapter 的內容:

除了因為是繼承自不同 Base Class 所產生的寫法上之差異外,並沒有太大的不同。重點是在接收事件的訂閱多了一個,用來當資料異動的情境是修改時,只更新有異動的 Item,以增加程序運作的效率。

接下來的工作就是把 Adapter 整合到 MainActivity 的程式碼中:

除了把 Adapter 傳入對應的畫面元件外,還有幾個重點。第一個是在 onStop 時要呼叫 Adapter 的 dispose 以避免之前提到的 Memory Leak 的問題。另外一個是在 onStart 時會以非同步的方式要求提供 User 的清單資料,在畫面持續在前景的同時,UserStore 完成資料載入就會觸發 UserAdapter、UserAdapter 再觸發 Spinner、Spinner 觸發 TodoStore 的載入、TodoStore 觸發 TodoAdapter、TodoAdapter 觸發 RecyclerView 等一連串資料更新的動作。所以可以在 Spinner 的 OnItemSelectedListener 中看到要求送出 TODO_LOAD 的 Action。

會選在 onStart  都做一次資料載入的要求是考量到 Activity 被推入背景後,有可能會出現資料的異動,所以強制進行一次畫面的刷新。

寫到這裡除了執行所有已完成的單元測試外,其實可以再回去執行一次 UAT,這時可以發現已經開始有測試結果轉為通過了。

撰寫 Integration Test

在繼續完成需求之前,先插入一個有關測試上的說明,使用 Flux 的其中一個重要原因就是希望提高程式碼的可測試性。所以在這次的示範之中,選擇以 Integration Test 來展示 FluxJava 可以達到的效果。

就像一開始提到的,用 Robolectric 來測試 MainActivity 被定位成 Integration Test。主要的測試目標是要確認整合起來後 UI 的行為符合設計的內容,此時當然不希望使用真實的資料來測試,簡單的說就是要把 Store 給隔離開來。

要達到這個目的可以由 FluxContext 的初始化做為切入點,以 Robolectric 來說,他提供了一個方便的功能,就是可以在測試執行時以 Annotation 中設定的 Applicaton Class 取代原本的 Class。 就如同以下程式碼所示範:

而在 StubAppConfig 中就可以對 FluxContext 注入測試用的 Class 來轉為提供測試用的資料:

這裡使用 StubAppConfig 做為切入點的示範並不是唯一的方法,在實際應用上還是應該選擇適合自己專案的方式。

如果在執行 UAT 希望也使用測試的資料來進行,以 FluxJava 來說當然也不會是問題,達成的方式在本次的示範中也可以看得到。原理同樣是和 Integration Test 相同,是使用取代原本 AppConfig 的方式。只是在 Espresso 裡設定就會麻煩一點,首先要增加一個自訂的 JUnitRunner,接著 build.gradledefaultConfig 改成以下的內容:

所以在執行 UAT 與正常啟動的情況下,可以在畫面中看到截然不同的資料內容,即代表 Store 代換的工作確實地達成目標。
Production Test

 

撰寫新增 Todo 功能

在這次的示範中,達成新增 Todo 的功能就只是很簡單地在 MainActivity 加上 Add Menu,透過使用者按下 Add 後,顯示一個 AlertDialog 取回使用者新增的內容完成新增的程序。以下是 menu_main.xml 的內容:

接著在 MainActivity.java 中加上以下的 Method:

用來讓使用者輸入資料的 AlertDialog 是用 DialogFragment 來達成,以下是 Layout 的內容:

程式碼則是如下所示:

再來就是讓 MainActivity 可以在使用者按下 Menu 時彈出 AlertDialog,所以新增如下的 Method:

執行所有的測試,看測試的結果沒有通過的不多了,距完成只剩一步之遙。

撰寫關閉 Todo 的功能

從最後一次 UAT 執行的結果可以發現,仍未滿足需求的項目只剩下關閉 Todo 最後一項。要達成這一項功能要回到 TodoAdapter,將 onBindViewHolder 改成以下的內容:

最後,執行最開始寫好的 UAT,非常好,所有的需求都通過測試,打完收工!





2017/7/4

如何在 Android Studio 中引用不在專案目錄中的 jar 檔

Android Studio 使用 Gradle 做為建置的工具在 Dependency 的管理上,節省掉很多煩人的工作,讓引用 Library 變成極為單純的作業,也讓開發人員可以更專注在重要的事項。透過 Gradle 的功能,要引用 Project 以外的 Library,只要在 build.gradle 中提供 Library 的識別名稱及對應版本編號,即可透過預先設定好的 Repository 來自動取得 jar,不需要再由人工下載、更新。

以下是一個簡易的引用示範:

就算是公司內部自行開發的 Library,也能在編譯、輸出 jar 後,上傳到私有的 Repository 中。再經由同樣的模式,在 build.gradle 中新增私有 Repository、要引用的 Library 資訊,就可以於 Coding 時使用 Library 中的功能。

例如:

縱使利用 Repository 的形式來管理 Dependency 可以滿足大部份的開發需求,但仍然有一些情境之下必須要使用獨立的 jar 來做為 Dependency。像是有一些小型的團隊,可能沒有足夠的資源來設置自己私有的 Repository,可是還是有跨專案共用功能的需求,這時就只能透過 jar 或共用 Android Studio 的 Module 來達成。

所幸在 Gradle 的設定中,除了使用 Repository 之外,還提供直接引用 jar 的設定方法。在 Android Studio 預設產生的 build.gradle 中都會有以下的內容:

這個內容代表了只要把 jar 的檔案放到 Project 結構中名為 libs 的目錄下,在 Coding 時就可以直接使用 jar 內提供的功能。

只是光這樣還是會有一些不足之處,若是以開發為業,勢必不可能只有一個 Project。團隊內部共用的 jar 在每開一個 Project 就要再複製一份到其下的 libs 目錄中,似乎不是一個好的 Dependency 管理模式,而把 jar 集中在所有專案之外的需求就應運而生。

這時就出現了一個疑問,原先的設定只有把 jar 放在 Project 內的 libs 之下,在 Project 之外的 jar 要如何設定 Dependency?

面對這一個問題,其實解決的方法也很簡單。之前 build.gradle 的例子中,dir: 'libs' 代表的是相對於 Project 的位置,也就是在 Project 之下的 libs 路徑。當 jar 在 Project 之外時,只要調整內容為 Project 之上的路徑即可。

以上面的例子來說,是假定所有的共用 jar 都被放在與 Project 平行的 shared 路徑之下,以下是目錄結構的示意:
+ Project A
  + app
    + libs
    + src
      + androidTest
      + main
      + test
+ Project B
  + app
    + libs
    + src
      + androidTest
      + main
      + test
+ shared
  - *.jar

透過以上的方式,就可以達成類似 Repository 的效果,當 shared 內的 jar 被更新後,Project A 與 Project B 都可以在 Source Code 中使用最新的功能。而當這二個 Project 所使用的 jar 有版本不一致的情況時,只要分別把對應版本的 jar 放到所屬 Project 的 libs 路徑下即可。










2017/6/12

Espresso 只做了半套的 Code Coverage

有在使用 Espresso 撰寫測試程式的人應該都知道,在 Android Studio 中 Android Test 類型的 Configuration 是不能使用 Code Coverage 的,最少在 Android Studio 2.2.2 仍然是如此。也就是說「Run 'xxx' with Coverage」的按鈕沒有辦法按,情況如下圖所示:


這點和 JUnit 的 Configuration 不同:



產出 Coverage Report 的第一步

以上的限制,對認真撰寫測試程式的人來說,會造成很大的不便。Code Coverage 是撰寫測試程式的一項重要指標,沒有了 Code Coverage 就如同在伸手不見五指的黑暗中行走,完全不知身在何處,有關更多 Code Coverage 用途的細節,請參考先前的這篇文章

不過,說是做了半套,代表並不是完全沒有辦法產生 Coverage Report。要在 Espresso 測試執行的過程中產生 Coverage Report,要先調整 build.gradle,在 buildTypes 設定中開啟 testCoverageEnabled 選項。

完成以上修改,並執行 ./gradlew createDebugCoverageReport 指令成功之後,就可以在 Module 的 /build/reports/coverage/debug/ 路徑下,以 Browser 開啟 index.html

以上的步驟在搜尋之後,都可以取到一堆的文件提供相關的操作細節。但本篇到這裡還沒有結束,這個方法僅適用於測試和待測對象在同一個 Module 中。如果是在不同的 Module 裡,由 Coverage 的 Report 頁面中,其實可以看出清單中的 Package 都只列出測試程式所在 Module 的 Class。


擴增 Coverage Report 的範圍

假設有一個如下所示的專案結構:
+ Sample
  + app
    + src
      + androidTest
      + main
        + com.sample.app
      + test
  + domain
    + src
      + main
        + com.sample.domain

其中 app 是 Android Application,domain 是 Android Library。如果把所有的測試 Class 放在 app 的 Module 之下,在 test 路徑下的測試可經由 Configuration 的 Code Coverage 畫面,來把 domain 中的 Class 列入 Report 產生的範圍。


androidTest 中的測試就沒有這麼直接了,以 Android 的 Gradle Plugin 官方文件內容來看,目前並沒有相關可調整的參數。所以這個階段的目標,就是讓 androidTest 產出的 Report 和 test 一樣,可以把指定位置的 Class 列入 Coverage 的範圍內。

要達到這個目標,首先要引入 JaCoCo 的 Plugin,透過 JaCoCo 的參數調整來突破原本的限制。在 JaCoCo 中有 sourceDirectoriesclassDirectories 二項參數,用來指定 Report 要涵蓋的範圍。以上面專案結構的例子來說,需要產生以下的內容在 app 的 build.gradle 中:

在這裡附帶說明一下,其實可以把以上的內容獨立成單獨的檔案,例如:jacoco.gradle,再透過 apply from: 'jacoco.gradle' 的方式引用。一來可以簡化 build.gradle 的內容、方便維護,二來可以避免調整 JaCoCo 的選項時,IDE 頻繁地出現要求同步的訊息。

另外有一點需要注意的是,createDebugCoverageReport 產出的 executionData 是以 *.ec 為名稱,與一般 JaCoCo 使用的 *.exec 不同。

設定好以上的內容之後,就可以執行 ./gradlew app:jacocoTestReport。和 createDebugCoverageReport 一樣會產出 html 格式的 Report,但不同的是 index.html 會在 app/build/reports/jacoco/jacocoTestReport/html 路徑下看到。這個路徑是預設的,可以透過參數調整。


修正 Coverage Report 的問題

到這裡所有的問題都解決了嗎?其實並沒有!照著以上的內容所產出的 Report 中,domain 下的 Class 不論測試怎麼做,在 Instruction 和 Branch 的 Coverage 都呈現 0%,所以顯然有一個環節出現了問題。

花了一番功夫發現 domain 在執行 jacocoTestReport 時都只會產生 release 的 Class。

就算是把 classDirectories 的路徑調整成 release,在產出的 Report 中 Coverage 依然是 0%。

目前解決方案是要調整 domain 的引用方式:

而在 domain 的 defaultConfig 下也要增加以下的設定:

使用以上內容再執行一次 ./gradlew app:jacocoTestReport,就可以由 Report 中看到,domain 下的 Class 不再全都是 0% 的狀態。


仍需改善的部份

雖然 Report 已經可以順利的產出,但是與 JUnit 可以在 IDE 中直接檢視 Coverage 情況的經驗相比,還是有差別。畢竟沒有辦法直接在 IDE 對照著 Code 就可以了解 Coverage 的情況,要在 IDE 與 Browser 間來回地切換其實非常地不方便。

原本有試著把 createDebugCoverageReport 產出的 executionData,經由以下【Analyze -> Show Coverage Data...】功能所顯示的 Window 載入。但這個功能似乎只能載入 *.exec 的檔案,就算是把 *.ec 改為 *.exec,載入之後 Coverage 的狀態也都呈現 0% 的結果。可能是真的有格式上相容的問題,所以才會把產出的 executionData 以 *.ec 來區格,避免被 Android Studio 載入。
只能期待未來 Android Studio 在改版時,能將這一部份的功能納進入,既然在 Plugin 都已經可以產出 Report 了,把 Plugin 的動作整合到 IDE 的 Coverage 按鈕上,應該不是什麼難事吧!
















2017/6/5

解決 Spock 與 PowerMock 的整合問題

在前一篇文章中,提到了如何在 Spock 中測試 Static 的 Method,以彌補 Spock 在這個部份的不足。當時使用的是 PowerMock 1.6.2,只不過隨著時間的推移,最新的 Mockito 與 PowerMock 組合,在與 Spock 的整合上並不順利。

Mockito 目前已經發展到第二版,但是要在這個版本的 Mockito 上使用 PowerMock,依據官方的說明仍然還停留在試驗性質的版本,目前最新可取得的版本是 1.7.0RC4。

如果使用前一篇文章提到的,以 @Rule 的方式來啟動 PowerMock:

原本可以順利執行的測試案例,在執行測試時會出現 NullPointerException

這個問題在網路上的資訊不多,如果無法解決,就要回到 JUnit + Mockito + PowerMock 的方案,原本 Spock 所帶來的優勢就失去了,是一個很兩難的抉擇。

所幸在研讀 PowerMock 的官方文件時,提到了一個新的功能。在 PowerMock 1.6.0 開始,PowerMockRunner 可以 Delegate 另一個 JUnit Runner,以取代無法使用 JUnit Rule 的情況。

而在 Specification 的 Source Code 中可以看到,Spock 是使用名為 Sputnik 的 JUnit Runner。

所以把之前的 Code 改成以下的內容:

build.gradle 可以更精簡:

透過 PowerMockRunner 的 Delegation,在 Spock 1.1 及 1.0 上,基本的功能都可以正常的運作,只不過會有以下額外的訊息出現。

Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
Notifications are not supported when all test-instances are created first!

由於不是 Production 的 Code,所以只要能運作,有點訊息或是使用的是非正式的版本,都還在可接受的範圍內。







2017/5/30

善用 Android Studio 的異動管理功能

身為一個開發人員,每天的工作就是在不斷地異動 Source Code 中度過。增加新的、修改舊的、刪掉不要的,而每一個異動都會對應到特定的目的,像是為了新的需求、修改 Bug、重構程式等等。

很多時候,異動的目的在工作的過程中是混在一起的,例如開發新功能的同時,也有可能在修正之前的問題。在自己的工作環境中,這些異動混在一起通常都不會有什麼問題產生。只不過這些工作的成果終究是要交付出去的,而問題總在於這些目的卻不一定是在同一個時間點被交付。如果所有的異動都混在一起,要隔離出需要交付的部份,勢必要花費一番工夫才能辦得到。

而這樣的工作要靠人工來逐個 Block、逐個 File 來分辨識,不但耗時,同時也極有可能出現疏漏。因為一個修改就有可能牽涉到十幾個 Files,再加上 IDE 自動產生或管理的加一加可能就有成百上千之數。自己經手的異動都不一定能精確的掌握,更何況是數量在數倍、完全不是自己產生的內容。

人工應付不來,就得要靠工具的輔助。就如同在「如何寫好程式」一文中提到,善用工具是寫好程式的功課之一。以開發 Android 時所使用的 Android Studio 來說,雖然是由 IntelliJ IDEA Community 版本進化而來,但不代表功能上就很陽春。針對本文提到的問題,其實有內建了相當方便的功能,可以協助開發者解決這類工作上的問題。

Android Studio 提供的異動管理功能

Changelist

這是一個以 File 為單位,把異動內容給分門別類的功能。透過這個功能,可以把修改過的 File 進行分組。當有異動內容需要被交付時,可以直接以分好的組別為單位交付。像是要進行 Commit 時,則可以指定特定的 Changelist 來 Commit,不在分組內的 Files 則不會受影響。

要使用這個功能可以先進入 Version Control Tool Window,Menu 的位置在【View -> Tool Windows -> Version Control】。開啟之後可以看見如下圖示的內容:


在 Local Changes 的 Tab 中,可以看到有一個 Default 的字樣,這就是 Android Studio 預先產生好的 Changelist。如果沒有特別指定,所有被異動的 Files 都會被歸在這個 Changelist 之下。在操作上可以使用 Tool Window 中左方的按鈕來新增一個 Changelist,新增時可設定此 Changelist 為 Active,代表之後所有還沒被異動的 File,在異動後都會被歸到這個 Changelist 之下。

要在 Changelist 之間移動 File 也非常地直覺,可以使用拖拉項目的方式,或是在項目上按下滑鼠右鍵選擇【Move to Another Changelist...】即可。

當要進行 Commit 時,就可以在如下的「Commit Changes」畫面中,最上方的下拉清單選擇對應的 Changelist。

選擇不同的 Changelist 時,Changelist 的名稱會預設成為 Commit Message 的內容。

由於 Changelist 是以 File 為單位,所以會有一個限制是同一個 File 不能同時歸屬於二個 Changelist。一旦編輯了不在 Active Changelist 中的 File,Android Studio 就會出現以下的警告:


可以看見 Tab 上的檔名變成了紅色,這是 Android Studio 遇到異動衝突預設的反應,這部份可以透過點選畫面中最右方的按鈕來調整。

這時如果只是忘了切換 Active Changelist,可以選擇【Ignore】或是【Switch changelist】。但若真的是二個不同的修改項目都異動到同一個 File,那就得選擇一個適當的策略。

當修改的內容不會有交互的影響,也就是說二個修改項目的結果可以共存在同一個 File 之中,則可以選擇【Move changes】把 File 移到最先要被 Commit 的 Changelist 中。

反之,修改的內容是互斥的時候,就要先保留其中一個版本、還原回修改前的狀態後,再開始另一個項目的修改。這個方式在 Android Studio 中也有提供了對應的功能來達成,在這篇文章的稍後會提到。

Changelist 在使用的情境上,還可以用來區隔一定會修改,但卻沒有要 Commit 的 File。例如有一些程式運作時需要的設定檔,內容中記錄的是 Production 的參數,在開發時就必須要進行修改才能做偵錯。這時就可以預先新增好一個專用的 Changelist,把這類的 Files 在修改之後歸進去。未來在 Commit 時才不致一時疏忽,把開發環境的設定參數給 Commit,造成後續建置上的問題。


Label

Label 主要是作用在【VCS -> Local History -> Show History】的 Window 上,如下圖所示:
在 Window 的左側,可以看到第一個和第二個 History 項目中間,夾了一個 Sample Label 的文字,這個文字是使用【VCS -> Local History -> Put Label...】功能放上去的。

透過這個功能,可以在進行一些實驗性的調整之前,先標定好目前 Source Code 狀態。當調整不如預期時,就可以不用花精神去回想做了哪些的修改,再一一去做回復。有了 Label 就可以在 History 的清單中找到所標定的 Source Code 狀態,使用【Revert】的功能,直接回到調整前的狀態,相當地省事又有效率。


Shelf

字面上的意義就是架子,是一個用來擺放文件夾的架子。而文件夾則是前面所提到的 Changelist 的快照,所以當 Changelist 發生衝突時,就可以利用 Shelf 把 Changelist 當下的狀態保留起來,等到衝突的情況解決了之後,再把原本異動的內容還原回來。

要把 Changelist 放到架子上,可以從 Menu 中選擇【VCS -> Shelve Changes...】。
可以由上圖看到,畫面和 Commit 差不多。完成之後,會在 Version Control Tool Window 中多出一個 Shelf 的 Tab,同時被 Shelve 的 Files 會回到異動前的狀態。在 Shelf 的 Tab 上,可以管理 Shelve 過的項目,像是 Unshelve、Rename、Delete。

在 Unshelve 的過程中,如果沒有出現內容衝突,則會自動套用 Shelf 中保留的異動狀態。如果內容出現衝突時,則會顯示以下的 Window,要求決定所需套用的版本:
Shelf 除了應用在工作項目的切換之外,如果所開發的 Project 有多個 Branch,在 Branch 還沒有相互 Merge 之前,也可以使用 Shelf 來轉移、把異動過程套用在不同的 Branch 上。這一點在異動的 Files 數量龐大時,就可以顯現出效率上差別,一個批次就可以完成工作,不用再一個個 File 來比對,並且擔心是否有異動的內容遺漏了。


Patch

Patch 可以算是 Shelf 的外帶版本,外帶去哪?就是把異動的內容帶出 Android Studio 的環境之外。使用 Menu 中【VCS -> Create Patch...】的功能,可以把原本要新增到 Shelf 的項目,改為產生一個實體的 File。操作的畫面和 Shelve Changes 一模一樣,只是在按下 Window 上【Create Patch...】的按鈕後,會出現以下的畫面,以便指定 File 儲存的位置。
基本上 Shelf 的項目和 Patch 可以互換,在 Tool Window 中 Shelf 的項目上可以觸發 Create Patch 的動作,讓 Shelf 的項目轉成 Patch。反之,也可以在 Shelf 的 Tab 上 Import Patches 成為 Shelf 的項目。在產生 Shelf 項目和 Patch 時,還有一點最大的差異是 Patch 產生之後,並不會將內容回復到異動之前,而是維持修改後的狀態。

從 Menu 中選擇【VCS -> Apply Patch...】後,可以把 Patch 的內容套用回目前的工作環境中,套用的過程和 Shelf 差不多,遇到內容衝突時也同樣會出現相同的畫面,來決定要選用的版本。

在應用上,Shelf 的項目能做的 Patch 都能做,除此之外 Patch 還可以用來在不同的 Android Studio 環境之間移轉。可以用來將工作的狀態由公司的環境中移至家中的環境,以便在離開公司之後仍可接續未完成的部份。或者是可以把 Patch 交給不同的開發人員,用來進行協同合作、Review Code 等工作。


和版本控管工具的比較

如果在開發時使用 Git 做為版本控管的工具,其實以上的功能 Git 大多都可以做到。Android Studio 則是在原有的版本控管機制之外,提供不同的選項,對於不熟悉版本控管工具的人來說有莫大的幫助。而對於用慣了原本工具的人來說,要怎麼使用還是得看每個人的習慣、對工具的喜好程度。只不過在面對不同的情況之下,多學會一種工具的使用,在應對的策略上也能產生更多的彈性。








2017/4/24

使用 MVP 時在設計上的考量

在「FluxJava: 給 Java 使用的 Flux 函式庫」這篇文章中提到,設計中使用 MVP 最大的問題,是會讓不同的畫面形成一組、一組的 Class,但各組之間是獨立的。MVP 最基本的設計概念中,只描述了同一組內 Class 如何互動,並沒有提到組內的 Class 如何跨組與其他的 Class 互動。當設計上出現要跨組的情況時,就得要仰賴設計者的功力與經驗了。

就 MVP 的精神,View 要負責的工作,只是把 Presenter 送來的 Model 內容呈現在畫面上。並且,與使用者互動,接收使用者的意圖、收集使用者輸入的資料,再交由 Presenter 處理。至於其他與 Business Logic 有關的事,不會由 View 來經手。

畫面的切換由誰負責

在只有單一畫面的情況之下,看起來很合理、分工明確,在設計上應該是個無可挑剔的方案。只是當畫面一多起來,隨之出現了一個問題:畫面的切換由誰負責?

有問題嗎?View 是負責使用者互動的,當然畫面的轉換由 View 來做囉!

也對,以 Android 平台為例,發送 Intent 大多是在 Activity 或是 Fragment 上處理的,再自然不過了。等新的 View 被載入後,再去啟動與其配對的 Presenter、讓 Presenter 把資料送過來。流程上都還在設計的預想之內,跨組的工作的確就由 View 來完成即可。

在畫面與畫面的順序固定的情況下,看起來是沒什麼問題。如果畫面的切換要依據資料的狀態來決定呢?

剛才有提到,為了保持每個 Class 任務的單純性,View 應該與 Business Logic 無關。要讓 View 根據資料狀態來決定,某種程度上就是 Business Logic,這樣是不是違反了一開始提到的精神?

而且判斷時所依據的資料,很可能跟 View 要顯示的內容無關,又或者是一個複雜的邏輯,又更加深了是否該放在 View 上的疑慮。

Presenter 是否要跨平台

不放在 View 又要放在哪?Presenter 上嗎?

這應該是在簡單的 MVP 結構之下,大多數人的選擇。當整個結構中,就只有 Model、View、Presenter,自然是只能由 Presenter 來存取資料庫、負責資料處理邏輯。此時再多加一項,依據資料決定畫面切換方式,好像也沒有什麼不恰當。

先回到 Android 平台上,來看看這樣的安排會出現什麼情況。

Presenter 要能夠控制 Activity 的轉換,必須要取得 Context,這也意味著 Presenter 與 Android 平台綁在一起。所以當這樣的設計內容,要移到不同的平台上,Presenter 就有可能要面臨大幅度在設計上的修改。換句話說就是,把工作放在 Presenter 上,會將設計限制在特定的平台上。

把 Context 排除在 Presenter 之外,就可以避免這個問題了嗎?

就算是 Presenter 不直接控制 Activity 的轉換,只決定要切換哪一個 Activity,Presenter 勢必要有 Activity 的資訊,不管是 Type 或是 Class 名稱。換了一個平台,顯示畫面的 Class 還會是相同的名稱嗎?可以確定的是 Type 一定不一樣。

MVP 套用在 Android 上的問題

那就不要跨平台,大不了新的平台把設計再重做一次!

其實對 Android 平台來說,問題還不止如此。以 Master-Detail 的畫面配置當例子,不同螢幕尺寸的情況下,會有一個 Activity 和二個 Activity 的差別。

原本在大螢幕中,一個 View、一個 Presenter 就做完的事,到了小螢幕卻變成二個 View,那 Presenter 也要跟著拆成二個?

假設答案是肯定的,也就是說同一個 App 裡,同樣用途的畫面就做了三組 View/Presenter。不對,在 Android 的 Master-Detail 的範本中,Master 的 Activity 是共用的,那豈不變成同一個 View 有二個 Presenter 配對?

這樣的設計好像有點累贅,但真的要在這樣的設計下,把流程串起來也不是不行。不過,要由 Master-Detail 跳到其他畫面的工作,應該三個 Presenter 都相同,是不是要抽離出來,不要在 Presenter 裡做?

結果,畫面切換要由誰負責的問題又繞回到原點。

當有使用 Service 或 BroadcastReceiver 的需求時,又會引發不同的問題。

沒有套用 MVP 之前,都是很直覺地在 Activity 中進行 Service 的使用。Service 大部份是用來進行後端資料處理的作業,這樣的 Service 該由 View 來啟動嗎?不是應該透過 Presenter?

現在前端不適合啟動 Service,那該由誰接手?Presenter 嗎?

是比 View 合適的選擇,但這樣又會回到 Presenter 要不要獨立於平台之外的問題上。

再者,Service 完成作業之後,如果要以 BroadcastReceiver 的流程來通知外部。BroadcastReceiver 可以放在 Activity 上嗎?MVP 傳送資料不是都要透過 Presenter?Service 在用來處理資料時,算是後端,不用經過 Presenter 嗎?

MVP 設計的下一步

在「MVC 與 MVP 的抉擇」一文中提到,把 MVP 中的 View 視為 Sub System,其實並不是突發奇想。而是在導入 MVP 時,用來應對在設計上所碰到的諸多問題的一個環節。

如果要深入的說明整個構思的內容,由於篇幅可能會很大,未來在時間允許之下,會有更多有關這方面的文章來做討論。






2017/3/31

MVC 與 MVP 的抉擇

古人有句話說「十年河東、十年河西」,前幾年在 Android 開發上,MVC 的設計一直是眾人談論的重點。但是隨著時間的更替,MVP 似乎開始熱了起來,連 Google 自家的範例中都提供了 MVP 的設計樣板,MVC 反而不在示範之列。

這讓很多人會興起一個疑問是:

MVC 和 MVP 我該選哪一個?我真的該棄 MVC 改投 MVP 的懷抱嗎?

面對這樣的一個問題,讓我想到了星爺電影中的一句台詞:

爭什麼爭,把兩樣摻在一起做瀨尿牛丸不就得了,笨蛋!

聽起戲謔,但卻不失為一個設計上可以思考的方向。MVC 和 MVP 的示意圖相信很多人可以信手捻來,並且解說得頭頭是道。但各位可曾想過一個問題,這些示意圖轉成實際的 Class Diagram 之後,都只能各自對應到一個 Class?如果把 V 視為一個 Sub-System,在設計上是不是可以有更多的可能性?

舉個例子,下面的這個示意圖是以 MVP 為主體,然後 View 的角色以 MVC 代換進去。



同理可證,如果不想要套用 MVC,那可不可以用 Facebook 的 Flux 架構代入?好像也沒什麼不可以,換完之後就像以下的示意圖:



如此設計上的變化,帶來了某種程度上的彈性。假設今天有個系統原本是在 App 上開發的,但需要移往 Web 平台。而 MVP 的 Model 就像前一段所說,不是一個 Class,而是代表 Business Logic 的 Sub-System。當前端顯示平台在做轉換時,可以由示意圖中看出在 Presenter 這半邊的設計是可以保持不變的,也就是說在設計上可以保持最大程度的重用性。以上面二個示意圖來說,原本在 App 中是以 MVC 的架構來提供畫面的呈現,而移到 Web 平台之後,則可以改採適合 Web 的架構來作為畫面呈現的基礎,相關的 Business Logic 則不需要更改。

當然以上是針對設計的部份,如果連 Source Code 都要可以重用,則不可能這麼直接地就達成目的。以上面的例子來看,如果原本開發的 App 是在 iOS 平台上,我想應該不會有人想要用 Objective-C 來開發一個網站吧!在這樣的情況之下所有的 Source Code 勢必得重新來過,只是相對地比起其他全新網站的 Case 來說,省下了不少在設計上所需的工作。

不過要讓 Source Code 可以重用也不是不可能實現,只是在設計的階段就必須要做好規劃。一開始就要讓代表 Business Logic 的 Model 與 Presenter 的互動是以 Remote 的方式進行,如此一來,不論前端的顯示平台是什麼技術、什麼架構,都可以直接重用 Business Logic 的 Source Code。

知易行難,概念上並不複雜,在實作上卻是有很多的細節需要考量,未來有機會將另闢篇幅針對更細部的設計做進一步的討論。

再把焦點拉回到主題上,其實要選擇 MVC 或 MVP 並沒有人可以告訴你正確的答案。因為每個 Case 都具有一定的獨特性,沒有方案是萬用的,只有最適合的。以上面提到的設計手法來說,在某種程度上是把設計給複雜化了,如果你沒有跨平台的需求,又或者你的畫面簡單到只有一、二個,這樣的設計手法就顯得疊床架屋、累贅了點。

所以當你在擔任設計的工作時,要選擇哪一個架構對目前的 Case 來說是最適合的,是你責無旁貸的課題。這是需要經驗,也是需要時間去思考的。你的煩惱也是其他正在設計系統的人所煩惱的,而你要做的、能做的也只有從不斷地嘗試與調整中成長。








2017/2/8

如何做好專案結構規劃

在另一篇文章中說明了「規劃專案結構」的重要性,在這篇文章中則要來談談如何實踐。

決定結構的依據

在決定專案結構的分類方式時,不外乎是依 Feature 或是依 Layer 來設置所謂的 Package 或是 Namespace,一般都會與實體的目錄名稱做搭配,形成一個同步的樹狀結構。這二種分類的差別,其實說白了就是倒底是要依照 SA 還是 SD 的文件內容來做分類的基準。

在進行系統設計時,理所當然地會以 SA 文件為基礎來開展工作,因為系統分析本來就是做為設計之前的資訊分析與統整的工作。在這樣的前提之下,所設計出來的 Class 就注定會帶有 SA 文件分類的屬性。然而在 OOP 的原則之下,單一個 Class 不太可能負擔所有的工作,所以設計出一組 Class 用來實現 SA 文件所描述的功能是很常見的手法。當設計工作再演化下去,為了有系統組織設計的結果,就會在設計中導入 Design Pattern 或是 Framework,來試圖形成多個 Class 的群組。這時 Class 就具備了第二種的分類屬性,因為在每一組的設計中 Class 都會有特定的角色或定位來協助完成對應的工作。

在經過以上的說明後可以看到,大部份 Class 都最少有二種的分類方式。然而,專案或者是目錄的結構只能以一種視點來表達,抉擇就因此而產生。依 SA 文件會以功能面或是處理的資料類型為主,所以分類上就會形成類似 Customer、Product、Order 這樣的結構。依 SD 文件則會是以 Class 的角色定位為主,如果 SD 文件中規定要使用 MVC 的 Design Pattern,則分類就會出現 Model、View、Controller 這樣的結構。

決定結構的首要考量

至於依 Feature 或是 Layer 何者熟優熟劣,就過去的經驗法則,我本身是比較傾向依 Feature 來分類。不過,在這之前其實要先考慮的是程式實體切割的問題。怎麼說?在系統成長、擴張到極致時,勢必得導入分散式的架構設計,也就是程式是散落在不同的運行環境之中,並且大多都以網路為交換資訊的媒介。

在進入到分散式架構的設計之前沒有先預留好必要的彈性,進入之後又沒有足夠的決心打掉重來。接著下來在設計上的調整工作,對負責的人來說將會是一個相當耗費心力的過程。

在這個過程中資料傳遞的問題會是最大的障壁,很多理所當然的 Class 之間交換資料之手法,在移至遠端後就晉級到完全不同的次元。在不是分散式架構時,所有的 Class 共用記憶體,所以資料在 Class 間可以直接共享、存取。在跨設備交換時,則需要增加額外的程序來達成,不論是對資料進行包裝或轉換,不是單純地把 Class 分別放置在不同的實體中就可以順利的運作。

再來,需要進行的是:調整不適用分散式架構的設計內容。資料傳遞的過程變複雜了,原本的設計就有可能不敷使用,增加接腳、改變呼叫方式都是必經的過程。可見範圍的改變也直接衝擊著原有的設計思維,不像是所有的 Class 都被裝在同一個容器中,在跨設備進行遠端呼叫時不可能「看得見」遠端所有的 Class,只會有被設計要用來揭露的介面,所以碰觸到這些部份的設計都需要重新來過。有時候一些違反設計精神、便宜行事的做法,譬如讓不相干的二個 Class 逕行互相呼叫,在這種環境下就會被嚴格地指正出來。相關的問題一般都是潛藏在設計的各個角落,等到系統運作出了問題才會發現這樣的計設方式行不通。當系統出錯的情況經過幾次之後,對負責設計的人而言耗掉了心力不說,工作的品質也會面臨嚴重的挑戰。

既然分割這麼麻煩,那就不要分,所有的 Class 都往同一個專案丟,不就什麼事都沒有了?以結論來說,這不是一個謹慎的架構師會採用的策略。這個方式的好處除了在更新版本時,不用考慮各端點設備版本配對的問題、直接將所有設備用相同的檔案覆蓋過一次之外,我想不到還有其他的優點。

首先,在開發階段會碰到的問題是剛才提到的可見範圍的議題,開發人員會因為所負責的 Class 看得見其他遠端的 Class 而產生混淆,然後開始不停地質疑為什麼明明就在眼前卻不能直接呼叫。但其中的差別大概也只有負責設計的人才弄得清楚,光是解釋就要花掉不少的唇舌。

再來就是部署時,不見得每一個端點的設備都有足夠的硬體資源提供給程式運作之用。當所有的程式碼都放在一起,就會出現一個現象是不論在哪種設備上,所提供的程式檔案都是一樣的肥大,會出現最糟的情況是因資源不足而有執行不穩定的情況。如果是在行動平台上,就有可能會因為要下載的檔案過大而使 App 的下載率降低,導因卻是 App 裡塞了很多用不到的程式碼這種低級的問題。

在設計時容易被忽略的重點

在 ISO 27001 的定義裡,所謂的風險指的是威脅加上弱點的組合結果。沒有威脅就算全部都是弱點也無所謂,如同把一個人放到完全沒有病毐及細菌的環境中,即使免疫功能不正常也不會有致病的風險。反之,如果把一顆石頭放到充滿病毐及細菌的環境中,也不會有人擔心石頭有生病的疑慮,所以沒有弱點就算有威脅也不用擔心。

在網路世代中,設計系統如果不考慮安全議題,是一個不及格的設計。然而很多時候有關安全的需求並不會被載入 SA 文件內,以致安全防護的設計在有心無心之下被排除在外。而把所有的 Class 都丟在同一個專案中,這種便宜行事的做法就是一個很不安全的策略、只有不在意資訊安全才會選擇的方式。

延續之前的內容,當設備中所部署的程式中包含了許多不需要的部份,就會增加弱點出現的機會,即便這些程式片斷在正常的情況下並不運作。而網路攻擊的威脅則不可能會消失、甚至手法不斷的翻新,二者結合就會大大地提高被入侵的風險值。

換個角度來說,設計與開發安全防護機制是需要成本的,在沒有必要的情況下開發人員自然是多一事不如少一事,檢查寫得是愈少愈好。畢竟在分工程度較高的團隊中,開發人員不一定會接觸到部署的相關規劃或執行,自然不會對資安俱備敏感度。再加上 SD 的文件中沒有特別指明防護要做到的層級,產出的結果一定是只有最低限度。一旦這些程式碼被入侵者以不正當的手法執行,就有可能形成很大的安全漏洞。所以在部署的思維上應該要做到精確的配置、只提供必要執行的部份,以期減少安全上的風險。

在安全上,被侵入是一個議題,資訊的洩露則是另一個。假設在行動平台中所下載的程式中包含有 Server 端的邏輯,以目前行動平台對程式碼的保護等級來說,無疑是送給駭客一份大禮。在這樣的情況之下,就算行動端的防護做得再好,也能夠依據 App 中額外的 Server 端程式碼來按圖索驥,找到破解的方法。

實體分割的原則

那該如何規劃以進行實體分割?常見的三層式架構是一個很好的切入點,也就是把設計的內容切分為 Presentation、Business Logic、Data Access 三大區塊。主要是一般的部署策略中,硬體設備的配置也是以這樣的架構做為雛型。所以當資料傳遞的切割點以這些區塊的界線做為基準時,在部署規劃有異動時比較容易配合硬體架構上的需求。

在最開始就把程式碼以實體的方式分開,像是在 Visual Studio 裡使用不同的 Project 並以 Solution 來封裝,或是在 Android Studio 裡在同一個 Project 中分出多個 Module。依據不同開發工具的特性,可以做到一部份的早期設計預警的效果。像是之前提到可見度的問題,在分開後多少能降低開發人員在這方面的疑惑,減少其誤用的情況。同時也可以驗證所設計的內容,在上線被分開部署後基本的呼叫過程是可以順利地運作。如果是使用 Visual Studio 特定的版本,甚至提供了自動化檢查的功能,負責設計的人員只要把相關的限制輸入好,就能在編譯時顯示警告,防止開發人員沒有按圖施工,可以節省很多查驗的工作。

預先做好切割還有另一項好處,可以提高 Class 的重用率、累積團隊的技術資產、減少重工。因為在後續的開發工作中如果需要相同的設計,直接引用已經現有的獨立單元即可。如不是,則要在過往的程式專案中巡覽、在一堆 Class 中做挑選、複製的工作,而每一個新專案都要再重複一次這個循環。

當然,事情永遠不可能單純到依照原則切割後,所有的 Class 就會自動歸位,接下來要煩惱的是 Class 的分配問題。譬如決定哪些是前一段提到可獨立於三層結構之外的共用 Class、哪些負責顯示資料、哪些用於處理商業邏輯、哪些直接存取資料。定位明確的好處理,曖昧不明、模稜兩可的則是會讓人傷神。

以一個大家常用的 MVC Design Pattern 為例,View 毫無疑問地是要被放在 Presentation 層內,那 Controller 和 Model 應該要放在 Presentation 還是 Business Logic?在這裡賣個關子,留給各位看倌去思考,也歡迎留言一起討論。

分類原則的選擇

做完了實體的分割,就下來就是怎麼決定分類的原則。

就像一開始提到的,我傾向以依 Feature 為主,不過這樣的說法並不精確。還是那句話,這個世界還沒有單純到只用一種原則就可以搞定所有的事。剛才也有提到共用的 Class 應該要被獨立出來以便跨系統可以共用,既然是可以跨系統代表是系統間共同的需求,或是因應設計產生的慣例。系統間共用的需求會出現在 SA 文件中,但是因應設計產生的慣例在 SA 文件中不會有,如果要以 Feature 為主進行分類,這些 Class 被獨立出來之後怎麼分類?

那看起來是依 Layer 比較保險囉?不是!因為實務上依 Feature 在擴充結構、任務分配、版本控管、程式碼巡覽上還是比較有優勢。例如:在需求變更時,不同的變更項目可以被結構給分離出來,所以在發行版本時能夠更精確的選擇要異動的項目清單,並且把未完成的部份隔離在要發行的版本之外。

再舉一個例子,想像一下,當你的系統在分散式的架構上,某個提供服務的設備超出負載,在評估後決定要進行分割程式打散到不同的硬體上以平衡負載。這時的切割方式是依照 Customer、Product、Order 比較合理?還是依照 Model、View、Controller 比較合理?

所以最後的結論是:以 Feature 為分類主幹、在細部中包含依 Layer 分類的混合結構。當 Feature 的分類到了盡頭,則可以用 Layer 來接力分類。或是在分類共用 Class 時,於慣用的 Library、Utility、Support 名稱下,再以 Layer 做更細部的分類。





2017/1/30

為什麼應該要做好專案結構的規畫

首先,先說明一下「規劃專案結構」具體的工作內容是什麼。以二個常見的程式開發平台來說,不管是用 Visual Studio 來開發 .NET 的程式,還是用各種 IDE 來開發 Java 的程式,都會有一組用來輔助 IDE 進行程式專案管理的檔案、甚至會伴隨著一到數個特定的目錄結構。只不過,這些都不是這篇文章要涉及的部份。而是除了這些內容以外,由開發人員自行產生的檔案該如何妥適的進行分門別類的工作,就是這篇文章所提到「規劃專案結構」要主要工作內容。

對很多的從業人員來說,「規劃專案結構」可能是個無足輕重的工作項目,心裡會想:不就是開幾個目錄把檔案往裡面丟就完事了嗎?能編能跑才是王道!會這樣想,大部份應該都是以開發小型專案為主,所以好與不好的專案結構在效益上並沒有太大的差別。

舉個例子,如果你是網拍的賣家,打算做個小本生意,只有一人負責接單、發貨。貨品的種類和數量都不會很多的情況下,進來的貨可能就只是隨手扔在腳邊,不管怎麼堆只要有一套自己的邏輯就行了,不愁找不到貨。但是如果網拍的事業開始小有名氣了、要逐步地擴大規模,發貨的品項或數量都不再是一個人可以負荷得來時,就得要增加人手。一旦人手增加了,同樣的模式下問題就跟著出現。如果放任新加入的人隨個人喜好來擺放貨物、大家各做各的,要如何才能有效地掌握目前存貨的情形?況且人吃五穀雜糧哪有不生病,沒有統一的理貨方法,其他的人要如何接手?所以把例子的層級拉高來看,對於一個成熟的網購業者來說,倉儲就是一門學問,如何設計貨架以便分門別類地來存放貨品,是維持營運的重要環節。

同理可證,很多大型開發專案也都是由小系統開始,應該很少有人會聽到自己的老闆一照面就說:咱們來做個功能無敵豐富的系統唄!大部份應該都是:我有個想法,先做個小系統來看看市場的反應,如何?好吧,既然是個小系統,那開發就隨性一點,看起來有個樣子、功能正常就行了。

跟先前網拍的例子一樣,你怎麼知道你現在的小系統不會意外地發展成大系統?有些人也許會覺得:等到了適當的階段,再一次性地投入資源,進行重構就好啦!不要花時間在無謂的事情上。然而,系統的發展很多時候就像是溫水煮青蛙一般,等察覺到系統已經大到需要有一個較好的分類模式時,程式碼早就已經盤根錯節。在大部分的人都會有的因循苟且心態下,重構?別傻了,只要能夠如期交付工作就上天保祐了,哪有多餘的力氣去做吃力不討好的事。

好的專案結構也是工作品質指標其中一環,當程式碼依據著規則分門別類地放置,在後續的維護上自然會有一定程度的助益。不論是開發人員間工作上的指派、輪調,或是在降低新進人員的學習曲線上,都會比雜亂無章的專案結構要來得成本低。

另外一方面,在物件導向的原則之下,每一個類別的任務應該要儘可能的單純,其中的一個效果就是讓單元測試可以更容易地被開發。在測試程式的涵蓋率達到一定的程度,有自動化回歸測試的輔助,這時候談重構才會有足夠的利基。否則談到重構,最大的恐懼都是來自於對於系統穩定度的疑慮,在這種情況下自然不會有人甘冒風險去更動已經完成的部份。

這時合宜的結構規劃就可以輔助開發人員,做為類別內容是否過於複雜的衡量基準。如果每一個類別功能夠單純,則在分類上就會減少很多模稜兩可的情況出現。反之,則會因為類別負擔了多個任務,在分類上容易出現歧見,或是無法決定該放在哪一個分類之下。

所以古人說「慎始」,同時也說了「勿以惡小而為之」,養成良好的習慣是很重要的一件事。當你是架構師時「規劃專案結構」是你要思考的細節。就算是一人專案,也應該認真地考慮替自己設定一個可以因應系統擴大的準則,為未來承接更大的系統而做好準備。