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