儘管時有非議,物件導向進入主流程式語言是既定的事實。就本質而言,物件導向是種典範,然而許多開發者,都是從支援物件導向的程式語言認識這個典範,也就不自覺地被特定語法給限制。

然而,沒有語法支援,就不能物件導向嗎?語言的實作層面上,又是如何實現物件導向的呢?

資料複合體

如果要談論物件導向,那麼,我們就必須先談論「物件(Object)」!

多數的文件都會提到,物件是物件導向程式中的基本單元,封裝了程式與資料,然而,就實作層面來看,並不是這樣的。

就如同數學上的複數,是由實數與虛數組合而成,而就物件而言,單純就是整體地看待一組資料,也就是個資料複合體。

因此,像[5, 10]就算是個物件了,也許這代表了一個複數,或者是一個平面直角座標,端看開發者怎麼去接受、運算這類資料。當然,使用可以令這個物件的意圖更明確,或者進一步地,使用個鍵值結構來儲存,例如{'x' : 5, 'y' : 10}會更加方便。這也是許多語言會用來呈現物件的基本語法形式,然而,不代表一定必須是這種形式,才是物件。

像[5, 10]、或{'x' : 5, 'y' : 10},就是所謂被封裝的資料。基本上,資料的封裝並非不可見,而是整體地看待這組資料,私有性(private)這類強封裝則是為了工程、團隊合作才出現的要求,一些比較彈性的語言,如Python,就不強調私有性。那麼,程式上的封裝呢?只要能夠處理這組資料的任何函式(Function)都可以!

真正封裝程式的是函式,如果有一組函式,可以接受物件(資料複合體)進行運算,就可以說這組函式是物件的方法(Method),也就是說,函式與物件是可以分別看待的,只不過概念上會稱這類函式為「物件可用的方法」。例如,有個獨立的函式add(v1, v2),若v1為,要將add視為v1的方法,可以接受v2為,這種方式並非不行。

也就是說,就實作面上來看,物件可以與方法分離,那麼,許多語言在語法層面上,會有obj.someMethod()的寫法,又是怎麼回事?

其中的「.」可以看成是運算子,像1 + obj.someMethod(),底層實作上可視為 (1 + (obj . someMethod())),也就是,obj與someMethod()被視為兩個運算元進行運算,而且,「.」運算子具有最高優先權,沒有交換律,obj會是函式中運算時this、self之類變數參考的對象。

查找方法的依據與約束

物件導向上,會有類別(Class),然而,物件與類別間的對應,也不是絕對必要——在還沒有類別之前,物件也可以獨自擁有方法,例如,add(v1, v2)時,add在概念上可視為v1的方法,這時並沒有類別的存在。

只不過,若有一組物件,經常地會使用某組函式,在程式碼中個別查看哪些函式時,雖然可以被這組物件使用,但這種方式並不方便,於是,希望有個查找函式的專區,願意的話,使用個鍵值結構也就足夠了,例如,CoodClz = {'add' : add, 'minus' : minus},接著,每個物件可以賦予class查找對象,例如[['x', 5], ['y', 10], 'class' : CoodClz],在需要操作add或minus時,就會到CoodClz上查找。

而本質上是資料複合體的物件,一旦擁有了類似的資料結構,也就有了一組函式位於專門供查找的位置。

為了便於表示物件擁有的結構與行為(函式),我們可以稱該物件為CoodClz的實例,而這麼一來,在管理物件上,就方便許多了,因為只要定義、查看CoordClz,就可以得知或規範物件的結構與行為。

類別的本質就是便於管理與約束物件,然而,管理的另一面,就是失去某種程度的彈性,類別約束性越強(例如Java),語言就越不靈活。

有些語言在語法上,允許定義類別之後,繼續增、刪類別上的方法,就工程而言,不應該這麼做。但問題在於,語言不會是完美的,也應該是能演化的,就修補語言而言,這又很重要,所以,任何語言勢必都要權衡類別的約束與彈性。

例如,JavaScript某些程度上,在語言半成品時就推出了,因此可以觸及許多底層實作細節,原型(prototype)是查找方法的依據。然而,又如同一般物件,如果可以隨意調整,既得其利的同時,也蕙深受其害,無怪乎許多程式庫拼了命模擬出各種風格的類別,希望增加約束性,而在ES6,也有了標準的類別定義方式。

重用其他類別方法

既然現在已經定義了類別,作為方法查找的依據與約束,緊接著會面臨的是另一個問題:在不同類別上,我們會發現有些方法在實作上,出現了相同的交集,當重複流程就出現在不同的類別中,就維護的角度而言,並非好事。

熟悉某個物件導向語言的開發者,第一個想到的是「繼承」,然而一如軟體界中許多名詞,通常沒有嚴謹的定義,繼承也是!

而許多開發者談到繼承,直覺的想法是有個父類別,不過,這只是其中一種方式,也就是目前類別查找不到方法時,繼續往父類別尋找,尋找的方式不一而足——也許是基於類別的,也許是基於原型的,或者像Python是基於MRO(Method Resolution Order)等。

繼承的本質是重用其他類別的方法,因此,如果從這方面出發,「組合模式」也算是繼承的實現之一(而不只是模擬繼承),當被組合的物件執行方法時,查找本身所屬類別有無該方法,實質上,也是在重用類別上定義的方法。

只要能重用類別上定義的方法,都可以算是繼承,Go語言就大膽捨棄了類別、繼承等語法,然而如果想要,還是有各種方式可以實現類別的概念,也能實現出共用結構方法,就算語法沒有直接支援物件導向,實際上,只要有心,實現起來也可以是物件導向。

語言的物件本身若支援個體化,直接將某個(類別上的)函式指定為物件之特性,也是重用其他類別方法的方式之一,或者是在類別上開放增、刪方法,甚至是可透過單一的方法,像是Circle.mixin(Ordered)等,將其他類別上的方法,一次性地指定給另一個方法。這些都可說是實現繼承,重用其他類別方法的可行方式。

設計演化的方向

如果語言在語法上直接支援物件導向,那當然是很方便。然而,就算不支援物件導向語法,在必要時,並非無法實現物件導向。

就像〈你所不知道的 C 語言:物件導向程式設計篇〉(https://goo.gl/ELsTRC)中所提到的,「物件導向是一種態度」、「只要有心,Brainf*ck語言也能作OOP」。

更確切地說,物件導向是設計上一種演化的方向,有心或沒有心,指的是開發者有沒有持續地檢討設計,以及當時的需求下是否適合朝該方向演化,而不是一味地套用封裝、繼承的語法或術語。有心的話,就算不想著物件導向,演化的方向後來自然地朝向物件導向,也只是剛好而已!

作者簡介


Advertisement

更多 iThome相關內容