1. 程式人生 > >對面向物件的一些思考

對面向物件的一些思考

面向物件,則在基於物件的基礎上增加了多型性。所謂多型,就是可以用統一的方法對不同的物件進行同樣的操作。當然,這些物件不能完全不同,而需要有一些共性,只有存在了這些共性才可能用同樣的方法去操作它們。我們從 C++ 通常的實現方法的角度來看,A 和 B 在繼承關係上都有共同的祖先 R ,那麼我們就可以把 A 和 B 都用對待 R 的控制方法去控制它們。

為什麼需要這樣做?

回到一個古老的話題:程式是什麼?

程式 = 演算法 + 資料結構

在計算機的世界裡,資料就是一個個位元的組合;程式碼的執行流程就是順序、分支、迴圈的程式結構的組合。用計算機解決問題,就是用程式結構的組合去重新排列資料的組合,得到結果。為了從龐大的輸入資料(從 bit 的角度上看,任何輸入資料都可能非常的龐大),通過程式碼對映到結果資料。我們就必須用合理的資料結構把這些位元資料組合起來,形成數量更少的單元。

這些單元,就是物件。物件同時也包括了對它進行操作的方法。這樣,我們完成了一次封裝,就變成了:

程式 = 基於物件操作的演算法 + 以物件為最小單位的資料結構

封裝總是為了減少操作粒度,資料結構上的封裝導致了資料資料的減少,自然減少了問題求解的複雜度;對程式碼的封裝使得程式碼得以複用,減少了程式碼的體積,同樣使問題簡化。

接下來來看 基於物件操作的演算法。這種演算法必須將操作物件看成是同樣的東西。在沒有物件的層次上,演算法操作的都是位元組,是同類。但是到了物件的層次,就不一定相同了。這個時候,演算法操作的是一個抽象概念的集合。

在面向物件的程式設計中,我們便少不了容器。容器就用來存放一類有共同抽象概念的東西。這裡說有共同概念的東西,而沒有說物件。是因為對於演算法作用於的集合,裡面放的並不是物件實體,而是一個對實體的引用。這個引用表達的是,演算法可以對引用的那一頭的東西做些什麼,而並不要求那一頭是什麼。

比如,我實現一個 GUI 系統(或是一個 3d 世界)。需要實現一個功能——判斷滑鼠點選到了什麼物件。這裡,每個物件提供了一個方法,可以判斷當前滑鼠的位置有沒有捕獲(點到)它。

這時最簡單的時候方法是:把所有可以被點選的物件都放在一個容器中,每次遍歷這個容器,檢視是哪一個物件捕獲了滑鼠。

我們並不需要可被點選的物件都是同類,只需要要求從容器中可以以統一方法訪問每個元素的是否捕獲住滑鼠的這個判定方法。

也就是說,把物件置入容器時,只需要讓置入的東西有這一個判定方法即可。瞭解 COM 的同學應該明白我要說什麼了。對,這就是 QueryInterface 的用途。com 的 query interface 就是說,從一個物件裡取到一個特定可以做某件事情的介面。通常接下來的程式碼會把它放在一個容器裡,方便別處的程式碼可以幹這些事情。

面向物件的本質就是讓物件有多型性,把不同物件以同一特性來歸組,統一處理。至於所謂繼承、虛表、等等概念,只是實現的細節。

說到這裡,再說一下 COM 。COM 允許 介面繼承 ,但不允許介面多繼承。這一點是從二進位制一致性上來考慮的。

為什麼沒提 實現繼承 的事情?因為實現繼承不屬於面向物件的必要因素。而且,現在來看,實現繼承對軟體質量來說,是有負面影響的。因為如果你改寫基類的虛方法,就意味著有可能破壞基類的行為(繼承角度看,基類物件是你這個物件的一部分)。往往基類的實現早於派生類,並不能瞭解到派生類的需求變化。這樣,在不瞭解基類設計的前提下,冒然的實現繼承都是有風險的。這不利於軟體的模組化分離和元件複用。

但是介面繼承又有什麼意義呢?以我愚見,絕大多數情況下,同樣對設計沒有意義。但具體到 COM 設計本身,讓每個介面都繼承於 IUnknown 卻是有意義的。這個意義來至於基礎設施的缺乏。我指的是 GC 。在沒有 GC 的環境中,AddRef 和 Release 相當於讓每個物件自己來實現 RC (引用計數)的自動化管理。對於非虛擬機器的原生程式碼,考慮到 COM 不依賴具體語言,這幾乎是唯一的手段。另外 COM 還支援 apartment 的概念,甚至允許 COM 物件處於不同的機器間,這也使得 GC 實現困難。

QueryInterface 存在於每個 COM 介面中卻有那麼一點格格不入。它之所以存在,是因為 COM 介面指標承擔了雙重責任,既指出了一個抽象概念,又引用了物件的實體。但從一個具體演算法來看,它只需要對一組相同的抽象概念做操作即可。但它做完操作後,很可能(但不是必須)需要把物件放入另一個不同的集合中,供其它演算法操作。這個時候,就需要 QueryInterface 將其轉換為另外一個介面。

但是,從概念上講,讓兩個不相關的介面相互轉換是不合邏輯的。本質上,其實在不相關的介面間轉換做的事情等價於:從一個介面中取得對物件的引用,然後呼叫這個物件的方法,取到新的介面。

如果去掉了 AddRef Release (依賴 GC )以及 QueryInterface (只在需要時增加一個介面獲得物件的引用),IUnknown 就什麼都不剩了。那麼介面繼承也完全不必存在。

回頭再來看程式語言。

C++ 提供了對面向物件的支援,但 C++ 所用的方法(虛表、繼承、多重繼承、虛繼承、等等)只是一種在 C 已有的模型上,追加的一種高效的實現方式而已。它不一定是最高效的方式(雖然很少能做到更高效),也不是最靈活的方式(可以考察 Ruby )。我想,只用 C++ 寫程式的人最容易犯的錯誤就是認為 C++ 對面向物件的支援的實現本身就是面向物件的本質。如果真的理解了面向物件,在特定需求下可以做出特定的結構來實現它。語言就已經是次要的東西了。

瞭解你的需求,區分我需要什麼和我可以做到什麼,對於設計是很重要的。好的設計在於減無可減。

你需要面向物件嗎?你需要 GC 嗎?你需要所有的類都有一個共同的基類嗎?你需要介面可以繼承嗎?你為什麼需要這些?