儲存與暴露程式的內部狀態

物件導向的精神之一, 就是讓將物件的內部狀態暴露為屬性, 讓外部存取。 其實動態作用域也可以視為一種暴露, 甚至 貝爾實驗室九號計畫 中將程式暴露為檔案系統, 也是類似的概念。

傳統物件導向

物件導向,將函數歸屬於物件下, 不同的函數可以存取物件屬性交換資料。 而函數中的狀態可以不只記錄在函數內部變數, 還能由物件介面讓外部可以存取。

有些人寫程式會儘量用函數的內部變數, 避免將內部狀態暴露, 寫網頁 javascript 也多用 iife, 怕汙染全域變數。 我認為多數情況沒必要, 反而公開物件才能讓別人寫外掛來互動。

掛在同一個物件下的屬性, 就像各方法的共用變數,可以用來交換資料。 之前寫 http 的 api 時, 要解析 http request, 就把解析後的資料都存在同一物件下。 各方法互相呼叫時可以少傳很多參數, 因為需要的資料都掛到物件下了。

動態作用域

動態作用域也有類似的用法,動態作用域下, 有自由變數的函數,在不同的 scope 中呼叫結果就不同。 這樣有點像在呼叫不同物件的同一方法, 只是把 scope 當成存屬性的地方。

函數重用

動態作用域讓重用函數變方便。 函數執行結果本來是只會因不同參數變化, 這樣要重用就要把可能有變化的地方寫成參數,有點麻煩。 在物作導向下函數成為方法,會依呼叫的物件結果不同。 而動態作用域,則直接在函數外部指定即可。

或是用 keyword argument 的方式, 或是把函數當成一個可 config 的物件。

plan9

對於 io,unix 有二種哲學: (unix 本來就是這麼混沌。) 一切作為管道元素,接受 stdin,輸出到 stdout; 和一切作為檔案系統,裝置、硬體,像硬碟就是 /dev/sda

管道是 unix tool 的哲學, 將硬體作為檔案系統暴露才是整個系統的哲學。 其中很重要一個部份 /proc 則是到後期 linux 才實作, 也就是每個進程模擬成一個資料夾, 裡面各文件內容是環境變數、打開的檔案、stdio 之類的。

而 plan9 則更進一步,把網路介面也模擬為檔案系統。 連 cpu 也是可訪問的,還有傳說中的協議 9p。

閉包與物件

javascript 及一些腳本語言,天生帶有閉包特性, 讓函數間的互相呼叫變得自然。 主函數中用到的在外部宣告的副函數, 以閉包幽微不可見難以修改的方式互相連結。 優點是情況單一,靜態作用域故名思義, 就是檔案裡怎麼寫,程式就怎麼跑, 不會在執行時被動態改變。

但這也是缺點,讓程式難以重用, 物件導向的出現就是要解決這個問題。 一個設計得好的程式,應該要在不用更改內部原始碼的情況下, 在一定範圍內從外部載入、增加、修改行為。

很明顯以 javascript 來說,如果宣告了一堆函數, 最後只匯出其主函數,而主函數中呼叫的副函數 都是以閉包特性連結到那些沒有被匯出的副函數; 在只匯出了主函數的情況下, 也就無法修改、重新定義副函數。

如果讓主函數及副函數,都作為一物件的方法, 彼此透過物件的 this 互相呼叫, 就能輕易的用物件導向的繼承,在子物件只重寫部份方法, 其它方法也不用改變,呼叫的自會變成子物件上重寫的版本, 達到不改變原始碼,就從外部改變原程式行為的目的。 甚至以 javascript 這種動態語言, 還能在執行時才改變物件方法。

另外,聽說一些動態語言的物件, 是用函數與閉包實現的,像 lisp 系。 雖然有點好笑,有了先進的閉包, 再用閉包實現看似底層的動態的物件。 (其實我不知道物件應該怎麼實現, 但感覺應該比閉包底層, 像 C 都有像物件的 struct 了。)


總之我認為,與其把狀態壓抑在函數內部的內部變數, 不如把狀態介面物件的介面暴露在外。 連一些物件的 method 中的工具函數也能這麼做, 這樣就能在需要時覆寫該函數,而無需重寫原始碼; 雖然要對原始碼有一定了解才能這樣玩啦。