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 做更細部的分類。