iThome 網站首載:程式語言的特性本質(三)從消弭重複性看封裝、繼承、多型
程式設計在某種程度上都是在消弭重複性,以提高程式可維護性來控制軟體複雜度。若從消弭重複性來瞭解物件導向中封裝、繼承、多型,就可具體瞭解這些基本原則的作用。
- 封裝消弭了物件的重複行為
假設你用類別基礎的Java設計僅具有name與balance的Account類別,同事拿來建立多個物件,像是建立acct1並為acct1.name與acct1.balance指定值,建立acct2並為acct2.name與acct2.balance指定值...
Account acct1 = new Account();
acct1.name = "Justin";
acct1.balance = 200; // 請自行想像以上流程重複多次
acct1.name = "Justin";
acct1.balance = 200; // 請自行想像以上流程重複多次
你立刻發現同事建立物件後都作了重複初始流程。對程式設計者而言,「重複」並不是美德。例如同事若初始10個物件,假以時日若初始流程更改了,他就必須修改10個地方,毫無可維護性可言。你觀察同事初始物件的流程,在Account類別定義建構式將初始流程予以「封裝」:
Account(String name, double balance) {
this.name = name;
this.balance = 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;
} //請自行想像以上流程重複多次
if(amt > 0) {
acct1.balance += amt;
} //請自行想像以上流程重複多次
又發現重複流程了!如果同事要為10個物件存款,假以時日存款要求單筆至少要100元,那麼同事就得修改10個地方,於是你修改Account類別定義如下:
void deposit(double amt) {
if(amt > 0) {
this.balance += 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());
} // 請自行想像以上操作重複多次
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());
}
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(),就從原型物件借用
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)原則就是將重複出現的現象集中管理。從這個出發點出發,不僅較易理解物件導向中封裝、繼承、多型的基本原則,在語法或模型不支援物件導向的語言上,也可實現出封裝、繼承、多型的類似概念。