不記得從哪兒看到的一句話,大意是:面向對象的設計模式掩蓋了軟件設計其實是這樣一個事實:把模塊按照依賴關系,組織成有向無環圖。"無環”是一個重要的要求,即軟件模塊之間不要出現循環依賴的情況。更好的架構是模塊分層次,某一層的模塊只依賴比它低一層的模塊。另外,模塊間的依賴,也就是圖里的邊,越少越好,邊越少,架構越簡單。
每個模塊應該是一組方法的集合,也就是一個抽象數據結構。一種數據結構,實際上是由它上面的一組操作來定義的。比如整數,只要滿足整數的運算規則,這種數據結構都叫整數。所以,模塊應當只包含方法,這組方法完全定義了這個模塊。如果是Java語言,理論上每個模塊都應該是一個interface。
每個模塊可以有多個實現,具體采用哪種實現,是動態綁定的——也就是說,不是在編譯期,而是在運行期決定的。我提出一個觀點(也許有人提出過了,待考證),把運行期划分為“初始化期”和“運轉期”。在“初始化期”,需要為每個模塊指定一種實現,並且建立模塊間的依賴、引用關系。這個過程可以自寫代碼,也可以通過類似spring的框架完成初始化和依賴注入。初始化期完畢,軟件進入“運轉期”,這時軟件才真正進入運行,可以實現既定的功能。
在“運轉期”,模塊間的依賴應當只包括抽象方法,而於某種具體實現中的特有方法無關。做出這種區分后,應當把盡量多的操作放在初始化期,因為初始化期軟件尚未執行,而且初始化期只執行一次,無需考慮效率,可以進行復雜的操作和嚴格的檢查。而且無論是執行檢查、還是輸出日志,都不會產生很大的量。初始化期易於檢查、易於調試,尤其對於多線程程序,線程尚未運行,調試難度大大低於運轉期。
目前的編程語言里沒有提供區分“初始化期”和“運轉期”的特性,但是做出這種區分是有意義的。對於某個具體實現,比如一個class,內部的成員變量可以大致分成兩類,一類屬於配置變量,一類屬於運行狀態變量。兩者分別對應於初始化期和運轉期。比如一個連接數據庫的類,數據庫地址、用戶名、庫名屬於配置變量,在初始化期設定,並且一旦初始化后往往不會改變。而某次query返回的錯誤狀態,屬於運行狀態變量,它在運轉期不停地被改變。顯然,狀態變量使得軟件行為更不可預測,而且帶來並行安全性問題。我們希望狀態變量越少越好,最好是沒有。如果沒有的話,這就是一個所謂的“冪等性”模塊(即多次調用返回的結果是一樣的)。
通常的編程語言並不提供定義“配置變量”和“狀態變量”的語法,但是可以做個類比。如果類比java,可以把前者看成final變量。final變量在構造函數中賦值,並且不能改變。但是,有些配置變量不一定在創建對象的時候就能賦值,而是在創建對象以后、開始運轉之前被賦值。這樣Java就沒法區分了。spring中提供了類似的概念,它將接口和實現完全分離,並且使用xml文件完成初始化期的工作。
與成員變量類似,成員方法可以按初始化期和運轉期作類似的划分。例如對象A持有一個指向對象B的引用,我們通常會在A中提供一個類似'setB()'的函數,向A中注入B的引用。這就是一個典型的“配置函數”,它的作用是在初始化期建立模塊間的依賴關系。而模塊的抽象接口中定義的方法,通常是“運轉期”方法。
如果接口和實現在初始化期綁定后,這種綁定關系在整個軟件生存期不再改變(這種情況在工程中也是比較常見的,如果要替換實現,重新初始化即可),那么這種動態綁定完全可以放到編譯期執行,例如I是一個接口,A和B都實現了I。我們初始化一個A的對象,並且將I的變量指向A的對象。如果在I的變量的整個生存期里這種綁定關系保持不變,那么這在編譯器就可以確定。例如,可以把I里的所有方法直接替換成A里的方法,這樣省去動態綁定所帶來的虛函數查找開銷,不過這似乎沒有多大意義。此外可能有意義的一點是,如果這個過程放到編譯期,編譯器就可以進行更多的語法檢查。把錯誤盡可能的在更靠前的階段消除,能夠大大減少調試時間。
OO里的兩大核心概念:抽象和多態,前者用於解決模塊化問題,后者解決接口和實現的綁定問題。這里要解決的核心問題,是接口和實現的分離。至於為什么要分離,根本原因還是控制復雜性。一個模塊,在概念上應當是簡單的,而實現上也許很復雜,但是這種復雜被約束在了模塊內部,外部只能看到簡單的概念。所以,模塊的划分要合理。如果模塊數量過多,或者關系雜亂,甚至接口定義經常改變,那么使用的工具再好也是無濟於事的。