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)程式開發
樂多分類:網路/3C 共同主題:PHP 程式設計 工具:編輯本文
Ads by Roodo! 
回應文章
實現某個 interface 的 class,並不是 sub class。interface 並不是 super class,故沒有 sub class 可言。

供參考。
Posted by 同人 at 2007年08月24日 13:49
嗯嗯,瞭解。

不過一下子我也想不出該用什麼名詞耶...請同人老大指點一下吧。
Posted by jaceju at 2007年08月24日 14:11
就用 class 就行了,你用的"類別"也 ok。不然最多我會用欲實現某某 interface 的 class 來表達。

BTW,interface extends interface 不代表它們具繼承的語意(有時候是,又有時候不是,視設計者的設計語意而定),所以最好不要稱繼承,用衍生是比較精確的說法。
Posted by 同人 at 2007年08月24日 16:38
感謝同人老大的指點 :)

我在用詞上常常會比較模糊,是因為對這方面的知識的研究其實還不是那麼深入,所以要靠各位高手指正呀~~

大感恩~~
Posted by jaceju at 2007年08月24日 16:50
站主別客氣呀,

你的學習態度與精神是我所敬佩的,其實我不知道的也很多,只有不斷地相互學習吸收新知才能減少我們所不知道的,也讓生活更加充實呀。
Posted by 同人 at 2007年08月24日 17:02
interface:
當你想要支援單一或多個 interface 繼承, 或為了確定某一 marker interface 時.

abstract class:
當你打算提供帶有部份實作的 class.

在實務上及慣例上, 通常 abstract class 亦會 implements interface.
如 Java 中的 HashMap extends AbstractMap. 而 AbstractMap implements Map.
Posted by racklin at 2007年08月25日 00:08
To racklin:
感謝你這麼深入的說明,看來我真的還有好多要學的 :)

> 通常 abstract class 亦會 implements interface.

這個有想到過,不過想不出怎麼去解釋它的作用。不過經你這麼一提,我倒是想到了 SPL 裡面有類似的幾個類別,我再研究看看。

能跟高手討論真好~~有空再來多寫幾篇「磚頭」好了 XD
Posted by jaceju at 2007年08月25日 00:49
Hello~
1. >Abstract Class 和 Interface 本身都無法用來建構一個 object (物件實體)
物件(instance)不是由這些咚咚建構的,Interface 是一個 "規格",Abstract Class 是 "尚未完全實作的規格",如此而已,物件是是由系統建構的才對,只是建構是依據設計人員的設計規格來產生的。 我以前有寫一篇文章:「物件的媽媽是類別?」,可以參考之。 ^^
http://www.kenming.idv.tw/index.php?title=c_carp_object_c_aofaofa_m_ei_a_y_class_i&more=1&c=1&tb=1&pb=1

2. 那個實現(implement)介面的類別,稱之為 "具體類別(concrete class)" 。

3. 建議 jaceju 有興趣物件思維的話,有本書:「Ojbect Methods」,By James Martin and Odell 寫的,我在第 13 期書評會有介紹。 :-)
Posted by Kenming Wang at 2008年03月6日 14:54

To Kenming:

感謝克明大的解說~

> 物件是是由系統建構的才對
其實我是沒想這麼多啦 :p 這裡主要是說它們在程式碼中都不能直接被用來 new 出一個物件;語意不明的地方還請克明大多包含。
Posted by jaceju at 2008年03月6日 15:28
您的那個 Apache/PHP/MySQL 安裝在 Windows 環境的文件寫得很詳細,讓我很輕易的可以安裝好這些環境在我的 Vmware 上。
所以作點回饋。 :)

不過 jaceju 可能可以好好思考到底什麼是類別,什麼是物件, 這點比較重要喔。
Posted by Kenming Wang at 2008年03月6日 23:52