iThome 網站首載:程式語言的特性本質(二)類別與原型的物件管理學
程式撰寫目的之一,就是描述問題中的重複行為與結構並加以管理,如何在管理重複性的同時兼顧彈性,一直都是程式設計領域中值得探索的焦點。
類別基礎(Class-based)與原型基礎(Prototype-based)為物件導向的不同風格。前者在設計時先強調物件的種類劃分,之後根據劃分的類別建立具相同行為與結構的實例(Instance);後者先著重物件個體的行為描述,日後隨著程式演進再來擔心分類等問題。類別基礎與原型基礎的差異,在於是否允許物件個體化(Object individuation)帶來的彈性,這就好比管理上要求員工凡事遵照準則行動,或是允許員工展現規範外的差異行為。
- 類別規範了實例的行為與結構
在描述問題中重複出現的物件行為時,類別基礎作法是將觀察到的物件行為予以分類,在類別中定義每個實例可擁有的行為與結構,但實例允許各自的狀態,這就好比製作公仔前要先開模,每個模子定義公仔的基本形象,然而實際鑄造出來的公仔可以有不同的材質或顏色等。
在類別基礎的作法下,要規範類別的使用者較為容易。由於使用者建立的實例都是基於類別規範,實例間唯一允許的差異性只有狀態。在這樣的模型之,程式的穩定性較高,因為實例不會有其它行為或結構的可能性;程式的安全性也較高,除非修改類別定義,否則不用擔心使用者對某物件作了修改,從而影響另一使用者或甚至整個應用程式,也因此類別基礎的作法,適用於對可預測性要求較高的工程環境。
由於類別的限制,實例除了狀態外不允許有其它差異性,類別一旦定義與發佈,想更新類別的行為與結構就不是件容易的事,畢竟這意謂著引用舊有類別的使用者,應用程式將受影響而需要修改。類別基礎的支持者往往注重程式開發前的分析,在實際撰寫程式前,得花費許多心力來探查需求,界定類別能力與辨識類別間的關係,因為此階段的類別劃分與關係,往往決定了程式的架構,以及將來面對需求時可展現的彈性。
- 原型基礎下物件是自主學習個體
在描述問題中重複行為時,原型基礎的作法是先實作物件行為,必要時再針對一組行為實作過程重複的物件予以分類,目的在管理「指導物件能力的過程」,經由同一訓練過程的物件仍可透過其它管道,擁有個別的能力。
類別基礎與原型基礎的差別是物件的學習能力。類別基礎下的物件有類別與實例兩種,實例的能力總是由類別規範,不允許物件擁有額外習得的技能,狀態是物件唯一可擁有的差異性;原型基礎下只有一種原始物件,物件的能力都是透過學習而來,具體來說就是對物件新增特性或行為,每個物件的能力與狀態都是獨一無二。
舉例來說,JavaScript是支援原型基礎的語言,如果需要一個物件擁有某些特性或行為,就必須對原始物件加以指導:
var b = {};
b.radius = 3;
b.volumn = function() {
return 3.14 * Math.pow(this.radius , 3) * 4 / 3;
};
b.radius = 3;
b.volumn = function() {
return 3.14 * Math.pow(this.radius , 3) * 4 / 3;
};
由於指導物件能力的流程出現重複,因此可以適當地將重複流程予以封裝。例如若有b1、b2等物件同樣要透過以上流程進行指導,則可以如下避免撰寫重複:
function Ball(b, r) {
b.radius = r;
b.volumn = function() {
return 3.14 * Math.pow(this.radius , 3) * 4 / 3;
};
return b;
}
var b1 = Ball({}, 3);
var b2 = Ball({}, 6);
b.radius = r;
b.volumn = function() {
return 3.14 * Math.pow(this.radius , 3) * 4 / 3;
};
return b;
}
var b1 = Ball({}, 3);
var b2 = Ball({}, 6);
經由共同流程的指導,已擁有一定能力的物件仍可以進一步個別指導。例如:
b1.color = 'red';
b2.weight = 20;
b2.weight = 20;
原型基礎下的物件每個都是獨立個體,沒有類別來規範行為與結構,雖然像JavaScript這樣的語言,可使用類似類別基礎的語法來建構物件,然而實際上仍是指導物件能力的過程:
// 看似定義類別,其實是定義指導原始物件的流程
function Ball(r) {
// this會參考至傳入的原始物件
this.radius = r;
this.volumn = function() {
return 3.14 * Math.pow(this.radius , 3) * 4 / 3;
};
}
// 看似根據類別建構實例,其實是建立原始物件並傳入函式中進行指導
var b = new Ball(3);
function Ball(r) {
// this會參考至傳入的原始物件
this.radius = r;
this.volumn = function() {
return 3.14 * Math.pow(this.radius , 3) * 4 / 3;
};
}
// 看似根據類別建構實例,其實是建立原始物件並傳入函式中進行指導
var b = new Ball(3);
由於先描述物件的行為,原型基礎的作法極具彈性,可先就觀察到的行為進行實作,需求發生變化時也易於隨時增減物件行為,即使事先對物件的學習流程予以封裝並發佈,後續使用者在不滿意原有實作的情況下,也可以根據需求重新指導物件的行為或結構。例如:
b.volumn = function() {
return 3.14596 * Math.pow(this.radius , 3) * 4 / 3;
};
return 3.14596 * Math.pow(this.radius , 3) * 4 / 3;
};
然而這也是類別基礎的支持者不安之處:沒有內建機制可對物件行為與結構加以規範。在原型基礎下,使用者隨時可以修改物件,降低了程式的穩定性,既然可以對任何物件進行修改,通常也表示可以對程式核心物件進行修改,如此之來,引用核心物件的整體系統行為也會受到影響,進而引發安全性的問題。
- 原型模彷類別是一種慣例約束方式
類別基礎與原型基礎對物件能力規範的作法有著極大差異,習慣類別基礎的開發者面對原型基礎語言,往往急於尋求類別基礎語言中規範物件的類似方式,而未認清原型基礎的出發點在於支持物件個體化,因此對原型基礎語言產生誤解與誤用。例如直接將上述JavaScript範例中,Ball函式定義與new關鍵字的用法,看作是類別基礎語言中等效的類別定義與new建構語法;另一種常見誤用,就是在未瞭解原型基礎中物件學習能力本質的情況下,就嘗試以某種方式實作出類別基礎下繼承的概念,或沒有去理解程式實際需求,就直接引用套用某種模彷類別基礎的框架。
類似以上的錯誤認知,非但無法發揮原本使用類別基礎語言的經驗,也無法發揮原型基礎語言的真正彈性。原型基礎去掉了類別的約束,目的在換取物件學習能力上的彈性,然而對某些經驗不足的開發者而言,過大的彈性反而是一種危險,因而需要某些機制來約束。模彷類別基礎中的類別封裝與繼承概念,其實是一種慣例約束方式,原型基礎要模彷類別基礎的方式很多元,該採用哪種模彷方式,正意謂著必須思考程式中需要哪一種慣例約束。這也正是原型基礎帶給來的彈性之一:可以根據需求採用不同慣例約束,而非語法層面上直接限制物件能力。
模彷類別基礎的封裝、繼承方式可以很多元,然而也只是一個方向,另一個約束物件個體性的方向是採用包裹器。舉例來說,JavaScript的Prototype程式庫採用對核心程式庫增加或修改行為的方式,並以模擬類別基礎的方式來約束開發者的使用模型,好處是類別基礎的開發者在運用時較為熟悉,壞處是更動核心程式庫並非最佳實務作法。jQuery採用包裹器的方式,使用包裹器收集要操作的物件,開發者對包裹器操作,包裏器則對收集的物件改變狀態,好處是不更動核心程式庫,符合最佳實務,然而開發者必須額外學習包裹器的使用架構。
- 注重工程管理或尊重個體差異的決擇
類別基礎與原型基礎對物件的能力管理方式不同,有趣的是,正如現實世界中的管理哲學,亦有中庸之道的物件管理方式。舉例來說,Ruby是以類別為基礎的語言,但支援物件個體化的設計,即使物件是基於某個類別建構的實例,Ruby亦可巧妙地透過單例類別(Singleton class)賦予實例額外行為,然而開放此彈性的代價依舊是失去語法本身的約束力。正如現實世界的管理中,給予員工自由發揮能力的同時,必然要有些不成文約定,給予物件個體化能力,就必然要有某些慣例上的約束。
類別基礎偏重工程性,使用類別規範所有物件行為,在重維護的場合較具優勢,但語法彈性相較上自然顯得不足。原型基礎偏重物件個體性,可以輕易賦予物件不同的特性,有利於快速堆砌功能,但容易造成維護上的混亂,在應用於工程性偏重的場合,往往得採取某種慣例來約束物件行為,像是上述採用某種方式模擬類別或使用包裹器,避免開發者直接修改物件的行為與結構。