2009年6月30日 星期二

物件繼承的物件觀點

「欣郁是個人。」
「欣郁生了孩子。」
欣郁生了個孩子,表示欣郁是女人,會生孩子很正常。即使這兩句話並沒有提到欣郁是個女人,正常人也可以藉由常理推敲出最有可能的結論。但是,如果不加入過去的經驗,純粹從以上兩句話來推敲,則會得到「人會生孩子」這種以偏概全的結論。正常人會從常識中選擇合理的解釋,但對沒有常識的程式語言編譯器而言,這種結論是上述兩句話的正解。這就是自然語言與程式語言之間的鴻溝:有些東西你認為是常識,編譯器卻不懂你的意思。
這篇文章要談的是,你如何正確地將上述兩句話依照物件繼承的類別觀點一文中的類別圖規格轉換成Java語言。
如果我們直接將上述兩句話逐句翻譯成Java程式碼:

「欣郁是個人。」 → 人類 欣郁 = new 人類();
「欣郁生了孩子。」 →
人類 孩子 = 欣郁.生產();

這兩行程式碼大有問題!
首先,我們並沒有說清楚欣郁究竟是男人還是女人。如果欣郁是男人,那麼欣郁.生產()就不合理。因此我們必須把程式碼改為:

人類 欣郁 = new 女人();
人類 孩子 = 欣郁.生產();

這段程式碼看起來較合理,起碼我們已經說明了欣郁是個女人的事實。然而,這樣的程式碼仍然無法編譯成功!
大家可能認為現在欣郁「事實上」是個女人,因此欣郁.生產()理應合法,那為什麼仍然無法編譯通過?理由是,當你把「欣郁」變數宣告成「人類」型態時,我們就把欣郁的「女人」的特性忽略掉了。這就好像我們說「人類是一種動物,所以人類會覓食」時,是把人類一般化成動物,而忽略掉人類獨有的特性(例如交談)。反之,我們不會說「人類是一種動物,所以人類會交談」因為這句話代表「動物會交談,而人類是一種動物,所以人類會交談」這樣的邏輯當然是錯誤的。同樣的道理,「女人會生產」並不代表人類都會生產。現在如果把欣郁一般化成人類看待,則我們就不能去看她獨有的「生產」功能,只能看到人類共有的「交談」功能。人類的定義裡並沒有生產的功能,而欣郁是人類,欣郁.生產()當然不合理。要讓這段程式碼能夠成功編譯,我們必須把欣郁當成女人看待,因此修改成:

女人 欣郁 = new 女人();
人類 孩子 = 欣郁.生產();

這樣的程式碼就合法了。
要理解上面的解釋或許很費腦筋,畢竟不是每個人的邏輯都相當清楚,幸好我們可以使用物件繼承的類別觀點一文中的功能觀點來清楚解釋每一個步驟。

new 人類();

可以用下圖表示:

※表示物件的功能觀點圖為了與表示類別的功能觀點圖作區分,使用較具立體感的顏色來表達「照類別規格產生的實體物件」的效果。
※本文將不會去區別參照與指標,因「指標」講起來比較傳神。
人類 欣郁 = new 人類();

則可以表達為「欣郁指標」指向「人類物件」:

現在就可以清楚看到欣郁.生產()是不合法的,因為圖中的人類物件根本沒有「生產」功能。

那麼,如果是這行程式碼,該怎麼解釋呢?

人類 欣郁 = new 女人();

如下圖:



※在此不管JVM內是否這麼實作,先從邏輯的角度剖析。

new 女人()產生的雖是女人物件,人類型態的欣郁指標卻是指向女人物件中屬於人類的部分,欣郁.生產()也就不合法,因為指標所指向的物件仍然不具有「生產」功能。
根據上述畫圖的原則,很自然可以推出下面這行程式碼代表的意義:

女人 欣郁 = new 女人();



由於欣郁指標現在是女人型態,指向女人物件的整個區塊,欣郁.生產()就變得合法。
同樣的道理也能套用在物件的轉型上,將在下篇文章說明。

2009年6月28日 星期日

物件繼承的類別觀點

從物件導向的術語來看,物件繼承是一種 「is a」的關係,但我個人比較偏好解釋成「is a kind of」,也就是「是一種」的關係,例如,男人是一種人類;女人是一種人類;人類是一種動物。這樣的形容比較精確,否則很容易和物件混淆。例如我們說,「欣郁是個人」,並不是說欣郁是人的子類別,而是說欣郁是人的一個物件,因此有必要釐清語意上的模糊。
將「男人是一種人類」、「女人是一種人類」、「人類是一種動物」的關係與特性以UML類別圖描述,則如下:


從類別圖的表示中,我們可以很清楚看見類別的階層關係。

一般而言,我們會很直觀的把上述類別圖轉換為如下的定義域圖,將每一個類別的定義視為一個集合。
但是我得告訴你一個壞消息:從這種角度思考,對物件導向概念的釐清沒什麼幫助。原因是:你最多只能從這張圖看出男人與女人處於類別階層的何種位階。物件導向著重的是類別所提供的操作,因此你最好將你的角度顛倒過來,像下圖一樣改為從功能性的角度去思考每個類別提供什麼樣的操作。


從這張圖你可以看見,男人與女人的功能裡包含著男人或女人特有的功能,加上人類及動物的功能。這樣你就不會期望男人與女人在交談上必須有一樣的表現,因為雖然交談是「人類」類別提供的功能,現在男人與女人都有自己的「人類」功能區塊,雖然都有「人類」的功能,卻可以提供不同的實作。這種Overriding(覆載)的概念,從功能性的角度才看得出來。

Dependency Injection

Dependency Injection(依賴注入,簡稱DI)是物件導向技術中常被用來降低模組間耦合度的做法,Martin Fowler首先在他的Inversion of Control Containers and the Dependency Injection pattern一文中使用了這個詞,並定義了三種型式的DI,分別是type 1:Interface Injection、type 2:Setter Injection及type 3:Constructor Injection,後來又由picocontainer定義了更多種類的DI。那些DI,其實我也不知道詳細情形為何,也懶得知道。無論如何,DI的用途就是「將模組之間的相依性從程式實作中抽離」。這樣說或許很抽象,但我們生活中其實充滿DI的影子。例如,每個MP3 Player一定需要電池才能運作,有些MP3的電池是內建的,無法更換;有些使用3號或4號鹼性電池,替換很方便。使用內建電池的MP3 Player,由於電池相依於MP3 Player的內部實作,如果之後電池壞掉,就只能買一台新的。而使用標準電池的MP3 Player,即使電池壞掉,只要到7-11買一副新的電池,馬上就能夠使用。同樣的道理也適用於軟體設計:萬一某天發現軟體中的一個類別有bug,究竟是要整個軟體都改過,還是只需要改有bug的類別?這樣我們就能歸納出一個結論:類別和MP3Player的電池一樣,最好能夠想換就換。廢話不多說,我們直接寫個Java MP3Player程式來看看。
class MP3Player{
private NormalBattery battery = new NormalBattery();
public void play(){
battery.usePower();
}
public int getBatteryPower(){
return battery.getPower();
}
public static void main(String... arg){
MP3Player player = new MP3Player();
/** 使用至沒電為止 */
while(player.getBatteryPower()>0){
player.play();
}
}
}
class NormalBattery{
private int power = 100; //預設電力100
public int getPower(){
return power;
}
public void usePower(){
power--; //每使用一次就遞減
}
}
Program A.

以上是代表MP3 Player與電池的類別。注意在MP3Player類別裡,battery欄位的初始值已經指名了要使用的電池是NormalBattery。這意味著如果哪天我們發現NormalBattery類別已經無法滿足我們的需求,想替換的話就必須連MP3Player類別一起更改。現在這麼看可能覺得只是小事,但試想若有10個類別同時使用到NormalBattery,你得花多少時間在這種猴子也能做的瑣碎工作上?
為了不讓身為人類的你退化成猴子,我們還是將上面的程式修改一下,並加入一個Battery介面。
interface Battery{
int getPower();
void usePower();
}
class MP3Player{
private Battery battery;
public MP3Player(Battery battery){
this.battery = battery;
}
public static void main(String... arg){
/** 建構 player時將NormalBattery傳入當參數 */
MP3Player player = new MP3Player(new NormalBattery());
....
}
}
class NormalBattery implements Battery{
....
}
Program B.

這Program B.裡,我們先將電池所共有的功能抽象成一個介面,並讓NormalBattery去實作它。在MP3Player類別裡,則讓battery欄位的值在建構時才由傳入的參數決定。如此一來,使用何種Battery介面的實作的決定權,便從MP3Player類別的實作者手中,轉移到MP3Player使用者的手上。就好比使用者可以隨意更換MP3 Player的電池,而不是取決於MP3 Player的製造商。
但是萬一MP3 Player用到一半,突然想把電池拆掉,又或者,一開始就不想要裝電池,該怎麼辦?如果是用建構子傳入參數的方式,因為沒有提供修改battery的方法,也強制MP3Player的使用者在建構MP3Player時就必須把battery的實體傳入。這種方式在某些情況下顯然不適用,因此我們改採另一種方法:不使用建構子傳參數,而是增加Setter方法。
class MP3Player{
private Battery battery;
public MP3Player(){}
public void setBattery(Battery battery){
this.battery = battery;
}
...
public static void main(String... arg){
MP3Player player = new MP3Player();
/** 設定電池 */
player.setBattery(new NormalBattery());
....
}
}
class NormalBattery implements Battery{
....
}
Program C.

Program C.中把改為不在建構子傳參數,而是去將參數傳入新增的setBattery()方法。這種作法在建構子參數太多的時候相當有用,特別是那些可以省略的參數。

介紹到這裡,看似頗為人滿意,終於可以快樂大結局了。但是,在這個能源耗竭的時代,我們必須思考著如果有一天,MP3 Player的價錢會跟電池差不多,只更換電池就顯得沒有意義,連MP3 Player也必須要可以替換才行。為了因應這種變態的要求,MP3 Player廠商終於決定不再販賣MP3 Player,宣布轉型為MP3 Player出租商,改成以服務的方式收取費用。
為了因應石油不足對產業結構產生的衝擊,物件導向技術也有一套作法,讓你不需要去理會程式碼寫了什麼或怎麼使用,只要改一改訂單,MP3 Player和電池就送到府上供你使用。這種做法必須仰賴介面,將各個實作類別抽象化,因此必須新增 Player介面。
class NormalBattery implements Battery{....}
interface Battery{
....
}
interface Player{
void setBattery(Battery batery);
void play();
int getBatteryPower();
}
class MP3Player implements Player{
private Battery battery;
public MP3Player(){}
public void setBattery(Battery battery){....}
public void play(){....}
public int getBatteryPower(){....}
}
class NormalBattery implements Battery{....}
import java.io.*;
import java.util.*;
class Main{
public static void main(String... arg)throws Exception{
Properties props = new Properties();
/** 從config.txt檔案中讀出屬性 */
props.load(new FileInputStream("config.txt"));
/** 動態載入類別 */
Class playerClass =
Class.forName(props.getProperty("player"));
Class batteryClass =
Class.forName(props.getProperty("battery"));
Player player = (Player)playerClass.newInstance();
Battery battery = (Battery)batteryClass.newInstance();
/** 設定電池 */
player.setBattery(battery);
/** 使用至沒電為止 */
while(player.getBatteryPower()>0){
player.play();
}
}
}

config.txt→
player:MP3Player
battery:NormalBattery
Program D.

由於Program D.要凸顯不需要知道MP3Player內部實作的特性,因此把main方法移到Main類別裡。現在整個主程式完全看不到MP3Player和NormalBattery的蹤影,但程式還是會呼叫MP3Player的setBattery()方法,並將NormalBattery的實體傳入。程式依賴於config.txt的設定,如果想要修改Player或Batter的實作,只需要將實作類別編譯好,接著修改config.txt的內容就可以了。這種方法很明顯要比前面的方法還要有彈性的多,然而如果實作是一些現成的框架提供的介面,在抽離框架時就必須大量修改,代價挺高。由於這種方法有很高的侵入性,大多框架還是使用前面兩種作法。

一開始曾經提到,Martin Fowler曾經定義出三種DI的型態。 Program B.是屬於type 3,Constructor Injection; Program C.是type 2, Setter Injection; Program D.為type 1, Interface Injection。定義得那麼複雜,其實一點也不難。

2009年6月27日 星期六

Digital Chaos

原本Logo右邊那個數碼圖是打算用Fireworks來畫的,直到開始畫才覺得這樣太浪費時間,又不見得好看,就想到用Processing來做,結果效果出乎意料的好。

原始碼:
int lineSize = 200;
int wide = 4;
int hei = 8;
int fontSize = 16;
int shadow_shift = 0;
void setup(){
size(800,800);
PFont font = createFont("Geogreia",fontSize);
textFont(font);
background(0);
translate(width/2,height/2);
for(int i = 1 ; i <= 5000; i+=5){
int rand = (int)random(2);
int num = (int)random(2);
int shift = (int)random(8) * (i/lineSize >=1 ? -1:1);
int x = i%lineSize+i/100*wide + shift;
int y = (i/lineSize)*hei+ shift;
float trans = 160+95*(shift/7);
fill(255,trans);
rotate((rand == 0? -shift:shift));
text(String.valueOf(num), x , y);
fill(255,trans*0.5);
text(String.valueOf(num), x+shadow_shift,y+shadow_shift);
rotate((rand != 0? -shift:shift));
textFont(font,fontSize+(rand == 0 ? -shift:shift));
}
}

言式法則

一直以來,我都想要開一個網誌來存放我那些用完即刪除的小程式,以及那些突然出現的關於程式設計的好點子。這個願望在教授冷酷無情的摧殘與自己無窮無盡的怠惰的雙重夾攻之下,每次都胎死腹中,不了了之。今天在500cc的咖啡因刺激之下,我終於下定決心按下建立網誌的選項。
總而言之,這網誌產生了,而這裡將會是我存放一些程式設計心得以及小作品的地方,可能偶爾會出現一些評論。如果你對文章不甚同意或有疑問,你第一件可以做的事情就是留言,然而喜歡保持網誌整潔的我,不能忍受網誌被留言霸佔,因此你的留言將不會出現在網誌上,系統會直接寄送到我的信箱,到時我會看到。如果你完全不想留言,你可以選擇馬上離開,這是乾脆又容易,同時也最被鼓勵使用的選項。
至於網誌的名字為什麼叫做言式法則?主要原因是作者個人對名字的喜好,次要原因則是這網誌裡的所有文章都是試驗性質的。如果你把言式兩個字合併起來看,就成為了「試」。沒錯,這個網誌就是秉持著試試看的精神創建的,因此若你發現這裡的方法對你無效,或者根本就錯了,請不要大驚小怪,就當和我一起試試看那些鬼東西,然後把問題回報給我知道,不然就乾脆離開,當作沒這回事。
這網誌裡的所有文章都不會標示「版權所有,翻印必究」的噁心標示,但這並不表示這裡的 一字一句都可以任意擷取,而是著作權根本是常識,如果要轉載或引用請一定要註明出處。其餘法律規章請參考Wikipedia之合理使用條文

很好,它終於誕生了。