2008年04月18日

Bridge 模式的簡單範例

今天跟 tokimeki 討論到 Bridge 模式的例子,這裡我做個簡單的記錄。

假設我們有軟體 (Software) 會透過印表機 (Printer) 做列印輸出,而軟體有文字編輯器 (輸出文字) 與影像編輯器 (輸出影像) ,然後印表機則可分做雷射、噴墨及點陣式。

在設計上,這兩者分別屬於不同的繼承體系,而 Software 又會利用到 Printer ,因此它們形成了 Bridge 關係,如下圖:

Bridge 範例

把幾個討論心得整理如下:

  1. 由於 Printer 抽象類封裝了實作上的不同,因此 Software 的子類別不必瞭解 Printer 的實作,它們只要知道 Printer 提供哪些介面可用。這些子類別會依照自己的需要來組合這些介面,就算用不到也沒關係。

  2. 在 Software 的子類別可以加入屬於自己的特殊方法,因為它們通常會依場合直接被調用,所以這裡並不需要遵守 LSP 原則。

  3. 對 Printer 體系來說,它不需要知道誰來用它;但反過來 Software 體系因為需要 Printer 體系的支援,所以形成了單向耦合。

  4. 對 Software 體系來說,只要它需要新的功能,而且這些功能所需要的實作也在 Printer 抽象類支援的範圍之內,那麼它就可以直接調用 Printer 物件的方法,而不會影響 Printer 體系。

  5. 如果 Software 體系需要的新功能是 Printer 抽象類所沒有的,那麼還是可以在 Printer 抽象類裡加入新的方法,而實作則交給 Printer 的子類別完成。這樣的修改完全不會影響 Software 體系。

更詳細的 Bridge 模式介紹可參考良葛格的「 Design Pattern: Bridge 模式」一文。


Posted by jaceju at 樂多Roodo!23:37回應(0)

2007年10月10日

[SVN] 如何同時讓多個資料夾與外部 SVN 同步?

遇到了一個 Subversion 的問題,記下來免得以後忘記。

問題說明

現在我有一個專案用到了 Zend Framework 和我自己維護的一個 MyLib 類別庫,而我想要讓它們都維持與官方 SVN 同步時怎麼做呢?

假設專案目錄結構如下:

app/
    lib/
        Zend/
        MyLib/

現在我要讓 Zend 這個資料夾與 http://framework.zend.com/svn/framework/trunk/library/Zend 同步,而 MyLib 則是與 http://localhost/svn/mylib/trunk 同步。

解決方案

Subversion 有個很用好的屬性稱為 svn:externals ,它可以用來指定讓某個目錄與外部的 SVN 同步,只是我之前僅會用到一個目錄的同步而已。例如我可以在 lib 目錄按下滑鼠右鍵,選擇 TortoiseSVN 的性質;然後在性質視窗建立一個 svn:externals 屬性,其值為:

Zend http://framework.zend.com/svn/framework/trunk/library/Zend

粗體字即為目錄名稱,然後空一格後面接著外部 SVN 的位置。

那麼如果要再多一個外部 SVN 的時候怎麼辦呢?我們只要在編輯原來的 svn:externals 屬性,並在原值的下方再加入新的設定值,也就是:

Zend http://framework.zend.com/svn/framework/trunk/library/Zend (換行) 
MyLib http://localhost/svn/mylib/trunk 

即新增粗體字的部份即可。

最後再利用 svn 更新一次,這樣就會讓 Zend 和 MyLib 資料夾和外部 SVN 同步了。


Posted by jaceju at 樂多Roodo!11:02回應(0)

2007年08月24日

[PHP] 該用 Abstract Class 還是 Interface ?

這裡把自己對 Abstract Class (抽象類別) 和 Interface (介面) 的理解記一下,如果有錯請指正我 :)

Abstract Class 與 Interface 的宣告方式

Abstract Class 和 Interface 本身都無法用來建構一個 object (物件實體) ,它們必須透過其他類別以 extends (繼承) 或 implements (實作) 來完成目的。在 PHP 中,我們可以用以下的方式來宣告一個 Abstract Class :

abstract class Test
{
    // ... code ...
} 

然後其 sub class 必須用 extends 來繼承它:

class ClassA extends Test
{
    // ... code ...
} 

而 Interface 則是用以下的方式宣告:

interface Testable
{
    // ... code ...
} 

然後 sub class 類別便可以 implements 關鍵字來實作該介面:

class ClassB implements Testable
{
    // ... code ...
} 

註:同人老大說實作 Interface 的類別不能稱為 sub class ,不過我實在也想不出什麼好名詞,歡迎大家提供意見。

另外比較特別的是, Interface 可以繼承衍生自另一個 Interface :

interface A extends B
{
    // ... code ...
}

這裡本來用繼承一詞,經同人老大指點後,用衍生一詞會比較精準。

但是除了語法上的不同,它們本身所代表的意義是什麼呢?

Abstract Class

當整個類別繼承體系有一致的行為時,我們通常會將這些行為抽離到上一層的 super class 去。如果這些行為的程式碼不在 super class 中定義的話,那麼會使得其 sub class 必須重覆地出現相同的程式片段,導致了壞味道的發生。然而有時我們並不打算讓這些 super class 可以被建構成物件實體,那麼它們就是所謂的 Abstract Class 。

在 Abstract Class 中, method 的定義方式有兩種,以下是有實作的 method :

public function method1()
{
    // ... code ...
}

值得注意的是,就算大括號中沒有程式碼,也算是有實作的 method ,我稱之為空實作

另一種是抽象方法,只要在 function 前加上 abstract 關鍵字,再拿掉大括號並加上分號:

abstract public function method2();

這樣的抽象方法,就會要求 sub class 在繼承之後要實作該方法的細節。

Interface

在我們確定了物件之間的溝通方式及規格,卻未能確定其實作細節時,就是利用 Interface 的時機。 Interface 能保證 Client 在操作物件上有一致的方式,而具體的表現方式則是讓實作的類別來決定。因此這些類別都必須實作在 Interface 裡定義的 method ,否則將會出現錯誤。在 Interface 中所有方法都是 abstract 的,定義方式和 Abstract Class 的抽象方法一樣,但是不用加上 abstract 關鍵字,例如:

public function method3();

顯然地,當 Abstract Class 所擁有的方法都是 abstract method 時,它就退化成了一個 Interface 。不過還是要小心以下兩點:

  1. 一個 Class 可以實作多個 Interface ,但只能繼承一個 Abstract Class ;這即是所謂的單一繼承體系,也就是子類別只能繼承一個父類別;但是一個父類別則可以被多個子類別所繼承。

  2. 在 Abstract Class 中可以宣告屬性成員 (attribute) 而 Interface 是不可以的,但兩者都能有常數成員 (constant) 。

範例

假設我們有兩個裝置 (Device) ,一個是鍵盤 (Keyboard) ,一個是滑鼠 (Mouse) ;而這兩種裝置都支援 USB 和 PS/2 兩種接頭 (Adapter) 讓使用者可以自行選擇,不過一次只能選擇一種接頭。

註:這個範例舉得不是很好,只是為了說明而已。

上面文字明白指出了我們可能會有 Device 和 Adapter 兩個 Abstract Class (或 Interface ) ,不過問題是哪一個應該用 Abstract Class ?而哪一個該用 Interface 呢?

為了簡化說明,我們把原來的問題重新定義為以下的規格:

  1. Device 有 inputData (輸入資料) 及 getStatus (取得狀態) 等兩個方法。

  2. Adapter 有 send (傳送) 和 receive (接收) 兩個方法。

  3. Device 必須透過 Adapter 來傳送或接收資料,

我們從規格的第三點可以得知,無論 Device 的類型為何,都會需要透過 Adapter 來收送資料,這就是一種抽象行為。也因此我們必須為 Device 提供一個 Adapter 類型的 $_adapter 屬性成員,從這點就可以看出 Device 應該是個 Abstract Class 。

而因為有 $_adapter 屬性,但我們又不想讓外界直接改變它,所以我們將它的 scope 設置為 private (私有屬性) ;然而要設定 private attribute 我們就要借重一個規格外的方法,這裡我將它命名為 setAdapter ;所以當我們在呼叫 setAdapter 時就必須傳入一個 Adapter 物件來指定給 $_adapter 屬性,這樣就能防止 Device 在使用 $_adapter 時的錯誤。


    public function setAdapter(Adapter $adapter)
    {
        $this->_adapter = $adapter;
    }

然後 Device 的 inputData 和 getStatus 就會委託 Adapter 物件的 send 及 receive 來收送資料:

    public function inputData($data)
    {
        echo $this->_deviceName, ' input data:';
        $this->_adapter->send($data);
    }

    public function getStatus()
    {
        echo $this->_deviceName, ' get status:';
        echo $this->_adapter->receive();
    }

註:以上程式還是有一些潛在的小問題,這裡就留給各位自行判斷囉。

對 Device 的 sub class 來說,以上的行為都是可以共用,也因此不必再實作一次了。完整的 Device 類別體系如下:

<?php

abstract class Device
{
    private $_adapter = null;

    protected $_deviceName = '';

    public function setAdapter(Adapter $adapter)
    {
        $this->_adapter = $adapter;
    }

    public function inputData($data)
    {
        echo $this->_deviceName, ' input data:';
        $this->_adapter->send($data);
    }

    public function getStatus()
    {
        echo $this->_deviceName, ' get status:';
        echo $this->_adapter->receive();
    }
}

class Device_Keyboard extends Device
{
    protected $_deviceName = 'Keyboard';
}

class Device_Mouse extends Device
{
    protected $_deviceName = 'Mouse';
}

接著再看 Adapter ,因為 USB 和 PS/2 在規格實作上是有所差異的,因此我們無法在 Adapter 中直接去定義像 Device 中 inputData 及 getStatus 這樣共用的行為方法。所以在這裡我們就能將 Adapter 視為是一個 Interface ,讓其下的 Class 去實作 send 和 receive 兩個方法的細節。所以整個 Adapter 的類別體系如下:

<?php

interface Adapter
{
    public function send($data);

    public function receive();
}

class Adapter_Ps2 implements Adapter
{
    public function send($data)
    {
        echo $data, ' by PS2.', "\n";
    }

    public function receive()
    {
        return rand(100, 200) . ' by PS2.' . "\n";
    }
}

class Adapter_Usb implements Adapter
{
    public function send($data)
    {
        echo $data, ' by USB.', "\n";
    }

    public function receive()
    {
        return rand(300, 400) . ' by USB.' . "\n";
    }
}

註:為了說明方便,所以我簡化了 USB 和 PS/2 的兩個方法的實作方式。

當然 Adapter 也不一定要是 Interface 才行,這得看整個程式架構上的設計來判斷。如果在 Adpater 在設計上會有共用的行為或屬性時,那麼 Abstract Class 就是比較好的選擇;只是在這個範例裡,我為了說明 Interface 的緣故,就大幅簡化了 Adapter 的設計。

整個設計用 UML 來表示的話,就是以下這樣子:

Device and Adapter

以下是測試的程式:

<?php

require_once 'Device.php';
require_once 'Adapter.php';

$keyboard = new Device_Keyboard();
$mouse    = new Device_Mouse();

echo "\n";
$keyboard->setAdapter(new Adapter_Ps2());
$keyboard->inputData('abc');
$keyboard->getStatus();

echo "\n";
$mouse->setAdapter(new Adapter_Ps2());
$mouse->inputData('def');
$mouse->getStatus();

echo "\n";
$keyboard->setAdapter(new Adapter_Usb());
$keyboard->inputData('abc');
$keyboard->getStatus();

echo "\n";
$mouse->setAdapter(new Adapter_Usb());
$mouse->inputData('def');
$mouse->getStatus();

/*
程式執行結果:
====================================================

Keyboard input data:abc by PS2.
Keyboard get status:130 by PS2.

Mouse input data:def by PS2.
Mouse get status:165 by PS2.

Keyboard input data:abc by USB.
Keyboard get status:360 by USB.

Mouse input data:def by USB.
Mouse get status:353 by USB.

====================================================
*/

完整範例下載

結論

很多時候我們常會搞不清楚該用 Abstract Class 還是 Interface ,其實這在設計階段是常有的事情。所以掌握以下的重點,就會比較容易看出兩者的使用時機:

  1. 當類別有共同的行為或屬性時,可以考慮使用 Abstract Class 。

  2. 當類別有共同的操作介面,但是實作上卻有所差異時,可以考慮使用 Interface 。

不過當我們發現整個類別體系用錯 Abstract Class 或 Interface 時也不用過於煩惱,這時「 Refactoring (重構) 」就是我們會需要的好幫手。更詳細的 Refactoring 說明可以參考以下書籍:


Posted by jaceju at 樂多Roodo!12:40回應(10)

2007年03月19日

[轉載] Readable regular expressions

上次介紹過 Regular Expression 五個良好的習慣,這次來個英文的相關文章。

範例是用 PHP ,不過我想其他語言應該也適用。

文章網址:Readable regular expressions (英文)


Posted by jaceju at 樂多Roodo!10:16回應(0)引用(0)

2006年07月10日

[轉載] RoR 風潮的背後

近來 RoR 話題不斷,以下這篇文章可以讓大家看看反面的意見。至於觀點對不對,我不予評論 (其實沒用過 RoR 的我也沒這個資格評論) ;因為對我而言,技術沒有絕對的對錯,只有使用的時機與環境是不是恰當。

文章連結:RoR 風潮的背後 (簡體中文) 。

重點提示:

從某種程度上來說,它掩蓋了問題的實質:領域對象的價值!

Posted by jaceju at 樂多Roodo!9:47回應(0)引用(0)

2006年04月26日

[轉載] 正則表達式的五個成功習慣

Regular Expression (正規表示式、常規表示式、正規表達式) 是很多程式開發者所懼怕的一種語法;但是因為它的功能強大,使得軟體界有個不成文的說法:沒學會 Regular Expression ,就不能稱為高手。

我是後來才學會 Regular Expression 怎麼寫的,不過我沒有因此而變成一個高手。但是 Regular Expression 讓我的程式功力大大的進步,而且在支援 Regular Expression 的文字編輯器中 (如 EditPlus 等) ,我也能夠很容易地找到一些俱備相同規則的字串。所以學會 Regular Expression 是一個好的程式開發者所必定會經歷的過程。

但是 Regular Expression 是有名的難懂,而且通常寫完之後就很難再看懂它要表達什麼;所以這次介紹的這篇文章,就很清楚地告訴我們要如何寫出讓人能夠容易閱讀的 Regular Expression 。

廢話不多說了,請自行參考吧。

文章網址:正則表達式的五個成功習慣 (簡體中文)

重點提示:

對於大部分程序員來說,在一個正則表達式環境裡使用空格和縮進排列都不成問題,如果他們沒有這麼做一定會被同行甚至外行人士看笑話。幾乎每個人都知道把代碼擠在一行會難於閱讀、書寫和維護。

Posted by jaceju at 樂多Roodo!22:08回應(3)引用(0)

2006年04月18日

[好站] Ruby 教學

關於 Ruby 的一些教學,因為國內還沒有什麼書可看,但是有些中文網站可以參考:

有看到其他的好站時再補充。

不過個人對 Ruby 的語法實在是很難完全接受, PHP5 比較接近我的習慣。不過可惜 PHP5 沒有 RoR 這種殺手級應用,這點希望往後能有所突破。

以下連結由 CFC 提供,感謝他 ^^

再補充:


Posted by jaceju at 樂多Roodo!14:22回應(5)引用(0)

2006年04月13日

鬼才 - 用文言文寫程式

用中文寫 Perl

我只能說...真的是去看到鬼...


Posted by jaceju at 樂多Roodo!23:56回應(6)引用(0)

2006年03月2日

物件導向不僅如此

我在藍色小鋪發表我的 ASP 物件導向心得之後,在矇矇的秘密基地裡也看到了一篇 <不要從程式語言學習「物件導向」> 。

其實這對我有很大的啟發,我以為我的文章已經描述了我在 ASP 上的物件導向思維,但是在我請教 Kenming Wang (就是 <不要從程式語言學習「物件導向」> 的作者) 後,他說了一句話:「您的 ASP 文章仍是藉由程式語言的層級來解釋物件的思維。」這對我簡直就是當頭棒喝!

我自己回頭看看我寫的東西,的確,我完全沒有提到我為什麼用這種方式來寫 ASP ,我只是賣弄我在 ASP 上所會的皮毛而已。看的人 (也就是大家) 一定還是不瞭解我所謂的物件導向思維到底是什麼。

雖然要承認錯誤很容易,但我想只這樣說說,完全是不負責任的作法。我的物件導向思維裡,容易擴充而易用性絕不能只是空口漫談而已,因此我想後面我會再補幾篇我精簡過的專案,用實際的例子來說明我這麼做的好處。

我想這邊要跟大家說聲抱歉,也感謝 Kenming Wang 的建言,他讓我上了寶貴的一課。


Posted by jaceju at 樂多Roodo!22:03回應(6)引用(0)
 [1]