程式設計在某種程度上都是在消弭重複性,以提高程式可維護性來控制軟體複雜度。若從消弭重複性來瞭解物件導向中封裝、繼承、多型,就可具體瞭解這些基本原則的作用。

封裝消弭了物件的重複行為
假設你用類別基礎的Java設計僅具有name與balance的Account類別,而同事拿來建立多個物件,像是建立acct1、並為acct1.name與acct1.balance指定值,建立acct2並為acct2.name與acct2.balance指定值……

Account acct1 = new Account();
acct1.name = "Justin";
acct1.balance = 200; // 請自行想像以上流程重複多次

你立刻發現,同事建立物件後,都作了重複初始流程。對程式設計者而言,「重複」並不是美德。例如同事若初始10個物件,假以時日,若初始流程更改了,他就必須修改10個地方,毫無可維護性可言。你觀察同事初始物件的流程,在Account類別定義建構式,將初始流程予以「封裝」:

Account(String name, double balance) {
this.name = name; this.balance = balance;
}

同事只要new Account("Justin", 200),就可以得到初始過的物件,就目前而言,你就為他節省20行程式碼的撰寫,假以時日,初始流程更改,你只要修改建構式,同事就無須作任何修改,即可大大地提升可維護性。

類似的狀況,假設同事想為多個Account實例進行存款:

Account acct1 = new Account("Justin", 200);
if(amt > 0) { acct1.balance += amt; } //請自行想像以上流程重複多次

這裡又發現重複流程了!如果同事要為10個物件存款,假以時日存款要求單筆至少要100元,那麼同事就得修改10個地方,於是你修改Account類別定義如下:

void deposit(double amt) { if(amt > 0) { this.balance += amt; } }

現在同事只要acct1.deposit(100),就可以完成存款動作,假以時日,存款流程更改了,也只要修改deposit()方法,同事無須作任何修改。

 

繼承消弭了類別間的重複定義

若觀察到多個類別間的出現重複定義時,可透過繼承來消弭重複定義的問題。

例如設計角色扮演遊戲時,先定義SwordsMan擁有name、blood等屬性,並為其定義了取值式(Getter)與設值式(Setter),再定義Magician擁有name、blood等屬性、取值式與設值式時,立即觀察到重複的程式碼出現了。

若有10個角色類別,倘若這些角色日後blood屬性要修改為hp,那得修改10個類別,這會有可維護性嗎?透過繼承,你可以定義Sprite擁有name、blood等屬性與方法,讓SwordsMan、Magician等繼承,日後這些角色blood屬性要修改為hp,也只需要修改Sprite類別。

在物件導向中,繼承不單是為了避免類別間的重複定義,還有「是一種(is a)」的關係,例如SwordsMan是一種Sprite,Magician是一種Sprite,這是判斷繼承是否適當的一個思考方向。除此之外,也可用「是一種」來瞭解多型的應用。

多型消弭了參考間的重複操作
假設你要設計方法顯示角色資訊,在不瞭解多型的運用前,也許會運用重載特性如下撰寫:

void show(SwordsMan s) {
out.print("(%s, %d)", s.getName(), s.getBlood());
}
void show(Magician m) {
out.print("(%s, %d)", m.getName(), m.getBlood());
} // 請自行想像以上操作重複多次

雖然參考的型態不同,但操作方式是重複的。若有100個角色怎麼辦?重載出100個方法?顯然重載不適合解決這個需求。如果Sprite與SwordsMan有繼承關係,可以撰寫Sprite s = new SwordsMan(),從右往左看的話,SwordsMan是一種Sprite,這是合法語句,也可以透過參考s對實例進行操作。將此觀念套用到方法參數上就是:

void show(Sprite s) {
out.print("(%s, %d)", s.getName(), s.getBlood());
}

這個方法可傳入SwordsMan實例,也可傳入Magician實例,因為Magician是一種Sprite。沒有多型前,若有100個角色,你要重載100個方法來解決需求,有了多型,只要繼承Sprite的類別,100個角色也只要運用這個方法就可以了。

 

從消弭重複性出發思考不同語言的實作方式

不同物件導向語言會有不同語法特性與模型,以原型基礎的JavaScript為例,雖然沒有類別概念,然而我在前一篇談過,若有兩個物件有著同樣的能力指導過程,可使用函式對該流程予以封裝。如果兩個函式定義了重複的指導流程,又當如何?得看你想依哪種方式消弭這個重複性。

若要利用JavaScript的原型鏈(Prototype chain)特性,就是準備一個已由該流程指導完畢的物件作為原型。例如:

function Sprite() {
this.getName = function() { return this.name; };
this.setName = function(name) { this.name = name; };
... 其他重複的指導流程
}
function SwordsMan() { ...SwordsMan特定的指導流程 }
SwordsMan.prototype = new Sprite(); // 以指導完畢的物件作為原型
var s = new SwordsMan();
s.setName('Justin'); // 實例沒有setName(),就從原型物件借用

實例上沒有的行為,我們就從原型上借來用,就消除重複的訓練過程定義而言,可算是繼承概念的實現。

至於多型概念的實現,前一篇文章中,我也談過動態語言由於變數沒有型態問題,只要思考參考的物件有哪些重複操作即可。

DRY(Don't Repeat Yourself)原則,就是將重複出現的現象集中管理。從這個出發點出發,不僅較易理解物件導向中封裝、繼承、多型的基本原則,在語法或模型不支援物件導向的語言上,也可實現出封裝、繼承、多型的類似概念。

 

作者簡介


Advertisement

更多 iThome相關內容