最近更新: 2009-11-11

PHP的中介編程與反射能力示範

本文是我試探不同程式語言的中介編程與反射能力系列文章中的第二部份,關於 PHP 的內容。 基本上,我指的是 PHP5 。

PHP 雖然被認為是動態語言,但語法上卻又具有許多靜態語言的性質,這使得它被認為「不那麼動態」。 尤其它在引入 OOP 能力時,學了不少 Java 語言的方式,因此限制了 PHP 的表達能力。 例如 PHP 也將型別分成原始型別(primitive types)與個體型別,套用 Java 的 class 與 interface 語法, 功能相似的反射類別等等。儘管如此,PHP 還是具有一些 Java 語言沒有的能力,故能達到比 Java 語言更高的靈活度。所以在實作本文所需範例時,仍比 Java 語言簡單。

區分原始型別與個體型別會造成撰寫程式時不必要的轉換工作,我在PHP5 的個體導向能力問題 - magic methods 和 interface中就以原始型別的 array 與個體型別的 ArrayObject (implements ArrayAccess) 為例,指出 PHP 未能平滑地將 primitive type 的方法(函數)與個體導向機制繫結。該文中指出的問題,在 Java 語言相當常見,例如 Auto box/unbox。但對強調OOP能力的動態語言而言卻無此困擾,即便是 C++ 程序員,也時常藉由運算子覆載或樣板消弭原始型別與個體的使用差異。

PHP5 增加類似 Java Reflection class 的反射類別,在效能上也遠比不上 PHP 傳統的反射函數,在PHP5 的動態函數/行為調用效率測試有我先前做過的測試數據。

本文所示範的輸出結果,與 JavaScript的中介編程與反射能力示範 相同。 程式碼結構也很類似,但是 PHP 本身就有提供 foreach ,所以省略了。 PHP 也透過 magic method 中的 __set(), __get()提供我們實作預設 setter 與 getter 的方式。 故而程式碼會比 JavaScript 版本的簡單許多。

Data.php, 第一個範例
<?php
/**
 * Data class
 */
class Data {
    // PHP5 的自識能力(反射能力)有點弱, property_exists() 並不能判斷
    // 這是類別內部的自我探知行為。
    // 基於本範例的目的,屬性的存取飾詞只能給 public 。
    public $id;
    public $title;
    public $content;
    public $timestamp;
    
    /**
     * @param $args 欄位初值
     */
    public function __construct($args) {
        foreach ($args as $name => $value) {
            // 用 $this 可以動態繫結,用 __CLASS__ 則是靜態繫結。
            // 會影響到子類別的行為。
            if ( property_exists($this, $name) )
                $this->$name = $value;
        }
    }
}

$d = new Data(array(
    'id'=>1,
    'title' => 'rock',
    'content' => 'hello world',
    'timestamp' => date('c')
));

echo "Properties of Data\n";
foreach (get_object_vars($d) as $name => $value) {
    echo " ${name}: ${value}\n";
}
echo "\n";

class DataDao extends Data {
    public $table;
    
    /**
     * magic method __get, __set 是 PHP5 新增的中介編程能力
     */
    public function __get($name) {
        echo "(magic get)\n";
        $realname = strtolower($name);
        if (isset($this->$realname))
            return $this->$realname;
        else
            return null;
    }
    
    public function __set($name, $value) {
        echo "(magic set)\n";
        $realname = strtolower($name);
        if (isset($this->$realname))
            $this->$realname = $value;
    }
}

$d2 = new DataDao(array(
    'id'=> 1,
    'title' => 'rock',
    'content' => 'hello world',
    'timestamp' => date('c'),
    'table' => 'Data'
));

$d2->Content = 'hello php'; // __set
echo "$d2->Content\n\n"; // __get
//為了和 public 的資料成員區別,特地用首字大寫表示。

echo "Properties of DataDao\n";
foreach (get_object_vars($d2) as $name => $value) {
    echo " ${name}: ${value}\n";
}

?>

第一個範例的寫法有 Java 的影子,只是受限於 property_exists() 而必須將屬性存取飾詞開放 public。但是後面設定 __set(), __get() 的作法,就比 Java 輕鬆多了。關於 __set(), __get() 的用法,我在 活用 PHP5 的 magic methods - __set(), __get() and __call() 有詳細說明,此處不再多談。

Data.php, use ReflectionClass

如果我們不想讓屬性可以被外部直接存取,我們還是可以將屬性存取飾詞限制為public or protected,再利用 PHP5 新增的反射類別來探知屬性名稱。而且此時更容易突顯 __set(), __get() 的好處。

使用了 ReflectionClass 的範例如下。

<?php
/**
 * Data.php (reflection 版)
 *
 * Data class
 */
class Data {
    protected $id;
    protected $title;
    protected $content;
    protected $timestamp;
    
    /**
     * @param $args 欄位初值
     */
    public function __construct($args) {
        $refData = new ReflectionObject($this);
        foreach ($args as $name => $value) {
            if ( $refData->hasProperty($name) )
                $this->$name = $value;
        }
    }
}

$d = new Data(array(
    'id'=>1,
    'title' => 'rock',
    'content' => 'hello world',
    'timestamp' => date('c')
));

echo "Properties of Data\n";
$rd = new ReflectionObject($d);
foreach ($rd->getProperties() as $property) {
    echo " {$property->getName()}\n"; //cannot access private data's value
}
echo "\n";


class DataDao extends Data {
    protected $table;
    
    /**
     * magic method __get, __set 是 PHP5 新增的中介編程能力
     */
    public function __get($name) {
        if (isset($this->$name))
            return $this->$name;
        else
            return null;
    }
    
    public function __set($name, $value) {
        if (isset($this->$name))
            $this->$name = $value;
    }
}

$d2 = new DataDao(array(
    'id'=> 1,
    'title' => 'rock',
    'content' => 'hello world',
    'timestamp' => date('c'),
    'table' => 'Data'
));

$d2->content = 'hello php'; // __set
echo "$d2->content\n\n"; // __get

echo "Properties of DataDao\n";
$rd2 = new ReflectionObject($d2);
foreach ($rd2->getProperties() as $property) {
    $name = $property->getName();
    echo " {$d2->$name}\n"; //cannot access private data's value
}
echo "\n";

?>
Data.php, use SPL Interface

使用了 ReflectionClass 的版本看起來有些礙眼,每次要傾印屬性值時都要另外配置一個反射個體。而且先前也說了,反射類別的效能比傳統的反射函數差。

事實上,PHP5有更靈活的寫法,可以同時實現資訊隱藏又不需要使用反射個體。進一步地,連屬性的數量(欄位數)都不必限制,可以在建構時動態決定。最重要的是,實現這麼多優點後的程式碼,比前兩種寫法更簡單、更優雅。

<?php
/**
 * 更靈活的 Data class
 * 不再限定欄位,並實作 Iterator 讓 foreach 的用法更平順。
 */
class Data implements Iterator {
    private $props = array();

    /**
     * 此資料類別直接透過建構子所接受的 hash table 內容,決定實體的可用資料欄位。
     * 不像另一版本會顯著列出欄位清單。
     *
     * @param $args  Hash table, 資料欄位與初值
     */
    public function __construct($args) {
        foreach ($args as $name => $value) {
            $this->props[$name] = $value;
        }
    }

    /**
     * magic method __get, __set 是 PHP5 新增的中介編程能力
     */      
    public function __get($name) {
        if (isset($this->props[$name]))
            return $this->props[$name];
        else
            return null;
    }

    public function __set($name, $value) {
        if (isset($this->props[$name]))
            $this->props[$name] = $value;
    }

    /**
     * PHP SPL Iterator function.
     */
    public function rewind(){ reset($this->props); }
    public function valid(){ return current($this->props) ? true : false; }
    public function current(){ return current($this->props); }
    public function key(){ return key($this->props); }
    public function next(){ next($this->props); }
}


$d1 = new Data(array(
    'id'=>1,
    'title' => 'rock',
    'content' => 'hello world',
    'timestamp' => date('c')
));

echo "Properties of d1\n";
foreach ( $d1 as $name => $value ) {
    echo " ${name}: ${value}\n";
}
echo "\n";


//別種資料結果
$d2 = new Data(array(
    'id'=> 1,
    'title' => 'rock',
    'create_timestamp' => date('c'),
    'update_timestamp' => date('c'),
    'table' => 'Data',
    'gid'   => 100
));

$d2->title = 'php';
echo "$d2->title\n\n";

echo "Properties of d2\n";
foreach ( $d2 as $name => $value ) {
    echo " ${name}: ${value}\n";
}

?>

這個最終實作版本應該是正統的 PHP5 寫法了(我說“應該”的原因是許多PHP程序員並不了解PHP5其實有能力幫我們做到這種程度,以至於他們寫的PHP程式碼像 Java 碼),它把資料成員完全隱藏起來,並實作 Iteratorforeach() 的用法更平順。所以我們的 foreach() 動作寫起來完全像是在走訪陣列。

動態語言在實作這種類別時,考慮到泛用性,通常不把資料成員/欄位寫死,而是允許動態數量欄位。這表示說不管我們向資料庫中查詢的資料結果(results)會包含多少欄位,我們都只用一個泛用的資料類別就可以處理。不必每碰一種資料結果就要再定義一個 model class。我上一句話指的就是 Java 語言最常要我們做的冗事。 C# 3.0 透過匿名類別解決了這個問題。

不過這個最終版本仍然暴露了 PHP 一個缺點,那就是不能直接擴充類別定義內容。這是靜態語言的性質,而不應該在動態語言中出現。所謂直接擴充類別定義內容,指的是不必用繼承就能在其他地方增加原有類別定義的能力。 Ruby 稱之為 open class,而 JavaScript 也可以透過 prototype 的操作實現此能力。甚至 C# 也可在事先規劃下藉由 partial class 的語法實現。

如果想要在 PHP 中嘗試 open class 的好處,那麼可以試試我在 PHP 實踐 mix-in 概念 part 2 - MixableClass 中實作的 MixableClass。它可以讓程序員在其他地方擴充類別與個體的行為,雖然實際用起來還不像 Ruby 那麼自然。

相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/10657709.html