顯示具有 物件導向技術之詮釋學 標籤的文章。 顯示所有文章
顯示具有 物件導向技術之詮釋學 標籤的文章。 顯示所有文章

2010年1月23日 星期六

淺談設計模式之觀察者模式

小明是個勤奮的台科大學生,平時相當努力在做一些與課業毫不相干的事情,例如在計算機網路期末考的前一晚還在研究Java的反射機制。如此的勤奮,以至於他在學期結束時發現有半數以上的老師不打算讓他過關。就這樣,小明被二一了。被二一的小明在學期結束後恐懼於父母的鞭打,隱藏自己提早畢業的事實,藉故留在台北,打算就這樣瞞天過海不理人間世事,用心鑽研Java。不料,紙終究包不住火,某日小明在宿舍享受著Java優雅的語法時,一陣急促的敲門聲打斷了他的思緒。小明不耐煩地開了門,還沒來得及看清楚來訪者的臉之前就先被一記大腳踹到牆邊,抬頭一看才驚見自己青筋暴現面目猙獰的老爸。這時,小明才終於意會到,老爸是自己在學資料的觀察者之一,同樣會被告知自己被二一的消息...
儘管可能有人好奇小明的下場,這畢竟不是這篇文章要講述的重點,這篇文章的重點是「觀察者模式」的實作,也就是說明在物件導向程式設計中,如何實作物件與物件的註冊與通知機制,而上述的人倫悲劇即是我們要實作的情境。小明的下場如何,請看倌自行想像。

觀察者模式包含兩種角色:「觀察者(Observer)」、「被觀察者(Observable)」。觀察者與被觀察者都能夠有多個:一個「被觀察者」能夠被許多「觀察者」觀察,一個「觀察者」也能一次觀察多個「被觀察者」。「觀察者」必須向「被觀察者」註冊,「被觀察者」在狀態改變或某些條件滿足時則告知「觀察者」,如下圖所示。


觀察者必須提供一個方法(update)讓被觀察者在狀態更新時呼叫,被觀察者也必須提供觀察者註冊(addObserver)及移除(removeObserver)的方法,如下介面所定義:
interface Observer{
/** 通知此觀察者*/
public void update(String message);
}

interface Observable{
/** 增加觀察者 */
public void addObserver(Observer observer);
/** 移除觀察者*/
public void removeObserver(Observer observer);
}
Observable介面的addObserver方法雖暗示了實作類別必須使用陣列或串列來儲存多個觀察者實體,但這並非強制性的,一個被觀察者也能只有一個觀察者,取決於整體的設計。Observer介面中的update方法所傳遞的參數也取決於使用的場合。若要設計出較一般化的Observer模式的實作,可以參考Java API中的Observer介面和Observable類別。

在實作上述情境前,
我們先整理出幾個步驟:首先,我們將「小明」與「小明的爸爸」註冊為「小明在學資料」的觀察者。其次,設定「小明在學資料」裡的分數。然後,學期結束時,「小明在學資料」會結算被當科目,確認小明有沒有被二一。最後,學期結束時,「小明的在學資料」會自動通知「小明」與「小明的爸爸」,小明是否有被二一。
以下是主程式:
class Main{
public static void main(String[] arg){
//小明的在學資料
StudentData studentData = new StudentData("小明");

People ming = new People("小明本人");
People father = new People("小明的爸爸");
//學生資料加入觀察者
studentData.addObserver(ming);
studentData.addObserver(father);

//設定成績
studentData.setScore("微積分", 59);
studentData.setScore("物理", 60);
studentData.setScore("計算機網路", 54);

// 學期結束,結算被當科目
studentData.validateCredits();
}
}
接下來是表示人(小明本人、小明的爸爸)的類別,實作觀察者介面:
class People implements Observer{
private String name;

public People(String name){
this.name = name;
}

public void update(String message){
System.out.println(
name + " 收到["+ message +"]的訊息。");

}
}
最後是表示「學生在學資料」的類別,實作被觀察者介面:
/** 在學學生資料*/
class StudentData implements Observable{
private String name;
/** 使用LinkedList來儲存多個觀察者。 */
private List< Observer> observers =
new LinkedList<Observer >();
private HashMap< String,Integer> scores =
new HashMap<String, Integer>();

public StudentData(String name){
this.name = name;
}

public String getName(){
return name;
}
/** 設定成績*/
public void setScore(String subject, int score){
scores.put(subject, score);
}

/** 結算學分*/
public void validateCredits(){
int totalSubject = scores.size(); //全部科目數
int failSubject = 0; //被當的科目數

/** 尋訪整個HashMap,取出所有的值 */
for(int score: scores.values()){
//不到60分,累加被當的科目數
if( score < 60)
failSubject++;
}
//若被當的科目數大於總科目數的一半
if( (totalSubject / 2) < failSubject )
notifyObservers("下學期不用繳註冊費。");
else
notifyObservers("下學期記得繳註冊費。");

}

/** 通知所有觀察者*/
public void notifyObservers(String msg){
/** 尋訪整個List,取出所有觀察者 */
for(Observer observer : observers)
observer.update(msg);
}
/** 新增觀察者*/
public void addObserver(Observer observer){
observers.add(observer);
}
/** 移除觀察者*/
public void removeObserver(Observer observer){
observers.remove(observer);
}
}
執行結果如下:
小明本人 收到[下學期不用繳註冊費。]的訊息。
小明的爸爸 收到[下學期不用繳註冊費。]的訊息。

Observer模式的原理其實很簡單:只要在被觀察者的狀態被改變(ex.set方法被呼叫)或條件滿足時,經由Observer介面所定義的方法,將狀態改變的消息傳遞給已註冊的觀察者們。然而,Observer模式有相當多應用,例如著名的MVC架構模式即是根基於此模式。關於Observer模式的各種應用,將在之後的文章提到,請耐心等待。

2009年7月23日 星期四

What is “this”?

What is “this”?這是許多物件導向程式語言初學者的疑問。
在許多物件導向語言中都存在著「this」關鍵字,舉凡Java、C++、C#都有這樣一個謎一般的關鍵字,在VB.Net與Ruby中則是以「self」之名存在著。某些非物件導向的程式語言如Javascript,甚至也提供「this」來模擬物件導向的特性。
究竟「this」是天使的化身,還是地獄的使者?是什麼樣的力量促使他在物件導向的世界裡屹立不搖?其實,它沒那麼偉大,充其量只是個「第一人稱代名詞」。
在解釋它存在的必要之前,我們必須先為一個人生難題找出解答:如何在不說「我」的情況下,用合理的文法表達「教授把我當了」?如果你說「教授把XXX當了」,這樣的句子很奇怪,像是說另一個和你同名同姓還被你爆料的可憐同學。如果你說「教授當了」,請問教授到底當了誰?如果你說「被教授當了」,聽起來合理,但這其實是把「我」省略的慣用句型,文法上不合理,因為缺乏主詞。
不知道有沒有人可以找出答案,如果有請留言告訴我。無論如何,代名詞不是我的重點,我只是想證實第一人稱代名詞的必要性:若沒有第一人稱代名詞,我們很難用語言表達自己的存在,也很難做第一人稱敘述。
在物件導向的世界裡也是一樣的。由於一個類別可以產生許多實體,就必須用一個代名詞來讓個別實體表達「我」,以區分和其他實體的不同。一個類別的實體就是用「this」表達「我」。

例如以下程式碼:
class Student{
private int score;
Student(int s){
score = s; //A
}
public void say(){
if( this.score > 59) // B
System.out.println("教授讓我過了!");
else
System.out.println("教授把我當了!");
}
public static void main(String[] arg){
Student 好學生 = new Student(80);
Student 混學生 = new Student(40);
好學生.say();
混學生.say();
}
}

在main中,產生兩個Student類別的實體,兩個實體say的結果不一樣,因為this.score的值不一樣。這樣就可以看出,每個new出來的實體都有自己的this,代表各自的實體。

※B的this是可以省略的,因為是在同一個類別,Java編譯器會在編譯時自動加入this。如A的score也可以改為this.score。

其實看下圖就一目了然:



main方法的Stack Frame中,「好學生」與「混學生」參照分別參考到不同的Student實體,兩個Student實體的this參照也分別參考到自己從屬的實體。如此一來,每個實體就可以用this表達「我」。例如,我的score就是this.score。
那麼你可能會有個疑惑:難道「this」存在的意義只是為了表達像this.score這種可以省略的語法嗎?「this」真正的好處在於,實體可以將自己傳遞給其他實體。

為了解釋這點,我們定義一個Professor類別,並在Student類別裡加入getScore方法,如下:
class Professor{
public void grade(Student student){
student.score = (int)(Math.random()*101);
}
}
class Student{
...
Professor 叫獸 = new Professor();
public int getScore(){
叫獸.grade(this); // C
return score;
}

public static void main(String[] arg){
Student 羔羊 = new Student(0);
羔羊.getScore();
羔羊.say();
}
}

我們先產生一個Student的實體「羔羊」,並呼叫getScore方法。呼叫時,羔羊的實體就會傳入叫獸.grade()方法,並設定score。C的部分,就是Student實體將自身傳入叫獸.grade()方法的部分。其實這行程式碼,翻成中文就是「叫獸給我打分數。」「this」就是「我」!

2009年7月1日 星期三

物件繼承的物件觀點-轉型篇

這篇文章是物件繼承的物件觀點一文的續集,將用物件的功能觀點圖來說明物件的強制轉型何時會成功。
首先,我們先釐清一些有關於物件轉型的概念。
在Java中,所有子類別的物件不需要強制轉型就能夠隱含轉換為父類別型態。因此以下程式碼皆能夠通過編譯,也能正常執行。

女人 欣郁 = new 女人();
人類 路人甲 = 欣郁;

人類 阿呆 = new 男人();



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。定義得那麼複雜,其實一點也不難。