程式設計在某種程度上都是在消弭重複性,以提高程式可維護性來控制軟體複雜度。若從消弭重複性來瞭解物件導向中封裝、繼承、多型,就可具體瞭解這些基本原則的作用。
封裝消弭了物件的重複行為
假設你用類別基礎的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)原則,就是將重複出現的現象集中管理。從這個出發點出發,不僅較易理解物件導向中封裝、繼承、多型的基本原則,在語法或模型不支援物件導向的語言上,也可實現出封裝、繼承、多型的類似概念。
專欄作者
熱門新聞
2025-01-06
2025-01-06
2025-01-06
2025-01-03
2025-01-03
2025-01-03