程式碼收納術


iThome 網站首載:程式碼收納術

程式碼的收納包括了邏輯層面與實體層面,前者指可執行程式碼組織的層次、名稱空間(Namespace)、能見度等,後者指原始碼檔案在檔案系統中的儲存、目錄結構等。無論語言本身是否內建模組(Module)或套件(Package)機制,在程式功能的組織與管理上,都有共同的考量要素。

組織功能時的層次考量

用來組織程式功能的層次,基本上可以是函式、模組、類別與套件。函式通常是可重用演算流程的最小單位,組織的重點在於避免(甚至禁止)副作用、邏輯泥塊,辦識出更高階的共用流程結構,以提高函式的可重用程度。當函式的職責夠清晰時,很容易會發現功能相關的數個函式,這時有兩個組織的方向:如果相關的函式牽涉狀態問題,可使用類別,否則可設計為模組。套件通常是模組的延伸,通常用來組織相關的數個模組,並要求實體檔案系統的對應。

深諳物件導向的開發者,面對必須將相關的函式組織在一起時,通常會使用類別,因為類別提供了功能的封裝與隔離。對於桌面應用、遊戲等程式類型,物件導向非常有用甚至是必要的,因為物件會有狀態,而且在記憶體中會有較長的生命週期。在「狀態」不是組織相關函式的考量因素時,同樣可提供封裝與隔離的模組,會是比類別更好的抽象層選擇。例如傳統Web應用程式中,採用無狀態的函式通常會是更好的典範,為的是避免在伺服端保存了相對於實體世界來說過時的狀態、避免共用存取的並行問題與效能負擔。

模組實際上提供了名稱空間。以Python為例,當函式func1位於模組module1中時,匯入(Import)之後必須以module1.func1來使用它;有些語言本身雖不具備模組機制,但可使用適當機制來模擬模組。例如Java中通常使用類別將數個靜態方法組織在一起,類別此時實際上相當於模組的角色。模組中也可用來組織相關類別,作為相關類別的名稱空間,在Java中會見到在類別中組織靜態內部類別,就是將外部類別作為名稱空間來使用。

模組與類別的選擇,除了考量狀態之外,「通用性」也是一個衡量因素。例如熟悉Java的開發者,往往不習慣用Python的len函式來取得資料長度,因為Java中是透過陣列的length或Collection的size方法取得,這實際上是因為在Python的認定中,len是個通用方法,可適用在任何具有長度概念的資料上,而不限定得是list、set等類型的物件,通常與物件狀態特定的功能,才會由物件擁有,像是list的sort函式,而sorted則被視為通用方法,因為它會傳回新的list物件,原有物件不受影響。

數個相關模組可組織為套件,在程式功能必須使用套件加以組織時,代表程式已有一定規模性,也就是具有相當數量的原始碼檔案必須管理,因此套件會要求檔案系統上的目錄對應。以Java為例,歸類於cc.openhome套件下的類別,其位元碼檔案必須置於類別路徑下cc目錄的子目錄openhome中。視語言支援而定,有些套件機制可管理模組間共用的功能。例如Python在套件機制上,規範對應的目錄中,必須提供__init__.py,當套件被匯入時,用以提供整個套件下模組的共同定義。

名稱空間與檔案管理的考量

程式規模龐大或程式庫彼此整合時,名稱衝突是必須面對的問題。如先前提到的,模組本身的作用,就是為變數、函式、類別提供名稱空間,只要所處名稱空間不同,名稱彼此之間就不會互相干擾。依語言支援而定,有些模組必須明確匯入,才能透過該名稱空間取用變數、函式與類別,例如Python;而Java沒有提供模組,而是提供套件,如前提及,套件是模組的延伸,作用也是提供名稱空間,而Java中即使沒有匯入,也可以透過完整名稱(Fully qualified name)來存取相關名稱。

名稱空間是為了管理,避免名稱衝突,管理就會帶來某些不便,如果名稱空間的階層數量多,程式撰寫時就免不了多打字,語言的匯入功能,通常有將名稱複製至目前名稱空間的功能,例如Python的from import,或Java的import,在不造成名稱衝突的前提下,適當使用可以節省程式撰寫時的打字功夫。有的語言則可在複製所有名稱至現有名稱空間時,選擇性地隱藏某些名稱,減少名稱衝突。要進一步考量的話,模組名稱也有可能發生衝突,因此可考慮在匯入模組時指定別名,像是Python就提供了import as語法。

在檔案管理的考量上,有些語言的模組與實體檔案是對應的,例如Python的一個py檔案實際上就是一個模組,檔案名稱就是模組名稱。Java雖然沒有模組,但公開類別名稱必須與原始檔案及位元檔案的主檔名一致,是類似的道理。模組與實體檔案對應的好處是,可從模組名稱直接得知原始程式碼是在哪個檔案之中。套件是模組的延伸,通常與實體目錄對應,Python與Java的套件基本上就是如此,這都有助於查找相關模組或類別。

能見度的考量

封裝的相對說法就是能見度,也就是類別或模組為外界可見的內部細節。在高度防禦性語言(Highly defensive languages)中,會提供權限控制機制來防止任何可能的誤用,例如Java在權限控制上,提供了public、protected與private,用以控制類別、方法或值域(Field)的名稱在不同套件或類別的能見度。

有些語言本身不提供權限控制,這並不代表設計時不需要「私有」概念,通常這類語言依賴在慣例,以Python為例,類別或模組中的名稱若以_開頭,就代表著不要直接存取,雖然還是可以透過某些方式來存取它,不過你不該這麼作,因為我們都知道自己在作什麼(We are all consenting adults)。如果有人違反了這個慣例,就得為自己的行為負責。

事實上真的必須採取防禦態度來提供私有特性,這類語言還是可以作得到,但與其用複雜的方式加以實作,這類語言的使用者通常寧可採取簡單的慣例來解決。想要控制能見度,除了依賴在慣例之外,使用清單來明確列出能見度,也不失為簡單且明確的解決方式。例如Python可以在模組中建立一個__all__變數,列出想要可以被其它模組from import的變數名單

重視模組與套件的觀念與精神

當語言本身沒有內建模組、套件、名稱空間、檔案管理、能見度等機制時,若能考量程式功能在組織與管理上的共同要素,也能依需求自行實作補其不足。JavaScript本身就是個極端的例子,以上談及的內建機制它都沒有,隨著前端需求的規模擴大,也產生了各種形式的模組、套件、名稱空間、檔案管理、能見度等模擬,像是近來能見度極高的RequireJS程式庫,就是個具體實例。

就如同沒有物件導向精神的人,即使使用物件導向程式語言,也會寫出完全沒有物件導向觀念的程式,而熟悉物件導向概念的人,即使使用C語言,也能寫出極為物件導向的程式。除了重視程式是否符合某種典範,是程序導向、物件導向還是函數式之外,開發者也應當重視模組、套件的精神與實作,學習如何設計模組的封裝,降低模組的耦合,增加模組的隔離性與獨立性。