最近更新: 2007-03-20

PHP 實踐 mix-in 概念 part 2 - MixableClass

第一部份《PHP 實踐 mix-in 概念之可行性》一文中解釋了 PHP 的個體如何加入新的方法。但那僅針對個體而非類別,那些混成內容無法繼承再用。而 Ruby 的混成(mix-in)概念是針對類別,其混成結果是一個類別,這些混成內容可經繼承機制再用。所以我接下來就要為 PHP 實踐一個可以混成的類別 - MixableClass

我的設計目標有二。第一、個體可以動態增刪方法,且不影嚮其他個體。第二、以抽象化方法混成新的類別。

個體可以動態增刪方法

我可以將某類別的實例視為獨立個體,僅為此個體增添方法,而不經類別關係影嚮其他同類或衍生類別之個體。舉例而言,當我配置了個體 $x 之後,我可以只為 $x 增加 foo 方法。而其他個體不論是否與 $x 同類,皆不會自動具有 foo 方法。這一點在第一部份《PHP 實踐 mix-in 概念之可行性》已經實現了。

以抽象化方法混成新的類別

可用「與特定類別無關的抽象化方法」混成新的類別,且混成類別的特徵仍然要與一般類別相同。

  1. 混成基礎類別的方法可為衍生類別所繼承。
    若我以 foo 方法混成了 MyClass 類,則繼承 MyClass 類之 MyClass2 類也會具有 foo 方法。
  2. 這些混成類別可以隨時增刪方法,且動態增刪之方法亦須依繼承原則運作。
    若我將 foobar 方法動態加入 MyClass2 類,不僅所有已配置之 MyClass2 實例將立即具有 foobar 方法,連其衍生類別 MyClass3 之實例也將依繼承原則而具有 foobar 方法。
  3. 遵循子承父、父不承子之繼承原則。
    當我動態加入 foobar 方法至 MyClass2 類後,衍生類別 MyClass3 將繼承 foobar 方法,但基礎類別 MyClass 不會繼承 foobar 。

在此有必要說明我所稱之「抽象化方法」為何?抽象化方法之意義與抽象類別 (abstract class) 或抽象方法 (abstract function) 不同。抽象方法與特定類別封裝在一起,且僅具函數簽名而不具有任何定義內容(沒有程式碼)。而我說的「抽象化方法」與特定類別無關但具有定義內容(有程式碼),可以將其視為純粹的演算法,是 Metaprogramming 中的一種概念。

混成類別: MixableClass.php

我設計了 MixableClass 實踐上述目標。在實作過程中,碰到了 PHP 動態能力不足之處,導致我必須多方嘗試並連帶影嚮實作結果的使用效能。這些狀況容我日後再提。

PHP 將方法混入個體或類別的方式,接近 C# 的委派方式,如同《Delegate in C# and Module in Ruby》所示。所以我以 delegate 表示混入方法。

<?php
/*
MixableClass - A PHP mix-in class
Copyright (C) 2007 Shih Yuncheng <shirock@educities.edu.tw>

This library is licensed under GNU Lesser General Public License.
*/
abstract class MixableClass {
    public static $methods; // object methods
    protected $_methods;    // instance methods

    /**
     * 為混成類別增刪方法。
     * @access public
     * @static
     * @param $o    類別名稱(字串),或是一個實例。
     * @param $m    委託者之名稱。
     * @param $fn   委派者之名稱。若為 false 則取消委派。
     */
    public static function delegate($o, $m, $fn) {
        if (function_exists($fn) or $fn === false) {
            $c = (is_string($o)
                ? $o
                : get_class($o)
            );
            //echo "\tadd method: {$c}::\$methods['{$k}'] = '{$v}';\n";
            $fn = ($fn === false
                ? 'false'
                : "'$fn'"
            );
            eval("{$c}::\$methods['{$m}'] = {$fn};");
            // PHP does not allow syntax as $c::$methods.
        }
        else {
            throw new Exception("$fn is not a function.\n");
        }
    }

    /**
     * 為實例之原型 (即其混成類別) 增刪方法。
     * @access public
     * @param $m    委託者之名稱。
     * @param $fn   委派者之名稱。若為 false 則取消委派。
     */
    public function prototypeDelegate($m, $fn) {
        self::delegate(&$this, $m, $fn);
    }

    /**
     * 為實例增刪方法。不影嚮其他個體。
     * @access public
     * @param $m    委託者之名稱。
     * @param $fn   委派者之名稱。若為 false 則取消委派。
     */
    public function instanceDelegate($m, $fn) {
        if (function_exists($fn) or $fn === false) {
            $this->_methods[$m] = $fn;
            return true;
        }
        return false;
    }

    /**
     * 以 setter 形式為實例增刪方法。不影嚮其他個體。
     * 如以下所示,兩者相同:
     * $x->instanceDelegate('myBar', 'bar');
     * $x->myBar = 'bar';
     * @access public
     * @param $k    委託者之名稱。
     * @param $v    委派者之名稱。若為 false 則取消委派。
     */
    public function __set($k, $v) {
        if (function_exists($v))
            $this->instanceDelegate($k, $v);
    }

    protected static function& getDelegatedMethod(&$o, &$f) {
        $fp = false;
        for ($c = get_class($o);
             $p = get_parent_class($c);
             $c = $p)
        {
            if ($fp = eval("return (isset({$c}::\$methods['{$f}'])
                ? {$c}::\$methods['{$f}']
                : false );"))
            {
                //echo "\t return {$fp} - {$c}::\$methods['{$f}'] \n";
                break;
            }
        }
        return $fp;
    }

    public function __call($f, $a) {
        if (isset($this->_methods[$f]))
            $fp =& $this->_methods[$f];
        else
            $fp =& self::getDelegatedMethod($this, $f);

        if ($fp and array_unshift($a, &$this))
            return call_user_func_array($fp, $a);
        else
            throw new Exception("Method {$f} not delegated!\n");
    }
}
?>

MyClass_test.php

<?php
require_once 'MixableClass.php';

function foo(&$This) {
    echo "FOO {$This->name}\n";
}
function bar(&$This) {
    echo "BAR\n";
}
function foobar(&$This, $n) {
    echo "FOOBAR ({$n}) {$This->name}\n" ;
}

class MyClass extends MixableClass {
    public static $methods;    // every subclass must define it.
    public $name;
    public function __construct($name) {
        if (!isset(self::$methods)) {
            self::$methods['foo'] = 'foo';
        }
        $this->name = $name;
    }
}

$x = new MyClass('Xman');
echo 'invoke $x->foo()', "\n";
$x->foo();

class MyClass2 extends MyClass {
    public static $methods;
}

$y = new MyClass2('Yman');
echo 'invoke $y->foo()', "\n";
$y->foo();

class MyClass3 extends MyClass2 {
    public static $methods;
    public function __construct($name) {
        parent::__construct(&$name);
        foreach (array('bar') as $fn)
            self::$methods[$fn] = $fn;
    }
}

$z = new MyClass3('Zman');
echo 'invoke $z->foo(), $z->bar()', "\n";
$z->foo();
$z->bar();

echo "========== 動態混入/委派 =============\n";
try {
    echo 'invoke $z->foobar("z")', "\n";
    $z->foobar('z');
}
catch (Exception $e) {
    echo "ERR: fobar 尚未混入 MyClass3 ; MyClass3 尚未委派 foobar 行為.\n";
}

echo "foobar 混入 MyClass2 ; MyClass2 委派 foobar 行為.\n";
$y->prototypeDelegate('foobar', 'foobar');
//MyClass2::delegate('MyClass2', 'foobar', 'foobar');

echo 'invoke $z->foobar()', "\n";
$z->foobar('z');
echo '子承父。MyClass2 的衍生類別 (MyClass3) 承繼其委派之 foobar.', "\n";

$y2 = new MyClass2('Y2man');
echo 'invoke $y2->foobar()', "\n";
$y2->foobar('y2');

echo 'invoke $x->foobar()', "\n";
try {
    $x->foobar('x');
}
catch (Exception $e) {
    echo "ERR: 父不承子。MyClass2 的基礎類別 (MyClass) 仍無 foobar 行為.\n";
}

echo "========== 實例行為委派,不混入類別中 =============\n";
$x->myBar = 'bar';
echo 'invoke $x->myBar()', "\n";
$x->myBar();

$x2 = new MyClass('X2man');

echo 'invoke $x2->myBar()', "\n";
try {
    $x2->myBar();
}
catch (Exception $e) {
    echo "ERR: myBar 並未混入類別\n";
}

echo 'invoke $y->myBar()', "\n";
try {
    $y->myBar();
}
catch (Exception $e) {
    echo "ERR: myBar 並未混入類別\n";
}
?>

執行結果:
invoke $x->foo()
FOO Xman
invoke $y->foo()
FOO Yman
invoke $z->foo(), $z->bar()
FOO Zman
BAR
========== 動態混入/委派 =============
invoke $z->foobar("z")
ERR: fobar 尚未混入 MyClass3 ; MyClass3 尚未委派 foobar 行為.
foobar 混入 MyClass2 ; MyClass2 委派 foobar 行為.
invoke $z->foobar()
FOOBAR (z) Zman
子承父。MyClass2 的衍生類別 (MyClass3) 承繼其委派之 foobar.
invoke $y2->foobar()
FOOBAR (y2) Y2man
invoke $x->foobar()
ERR: 父不承子。MyClass2 的基礎類別 (MyClass) 仍無 foobar 行為.
========== 實例行為委派,不混入類別中 =============
invoke $x->myBar()
BAR
invoke $x2->myBar()
ERR: myBar 並未混入類別
invoke $y->myBar()
ERR: myBar 並未混入類別

上例之類別繼承關係如下圖所示。

類別混成與行為繼承示意

各位不妨再看看「Prototype-based programming in PHP」以及「我也來實作 PHP mix-in 的概念」。他們的設計目標與我略有不同,可兩邊比較設計內容。

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

樂多舊回應
未留名 (#comment-4229167)
Tue, 20 Mar 2007 15:15:58 +0800
石頭成老大,看了你的程式後,我又想到一個有趣的實作方式,有時間我再來試看看。

不過我個人覺得上面的 delegate 方法如果改以接受 callback 虛擬型態的參數會不會比較 PHP 一點?類似 我也來實作 PHP mix-in 的概念 - Part 2裡的做法 (不過我倒是沒把一般函式考慮進去) 。

但是到現在還是沒想通 mix-in 在繼承以後能做些什麼事,只能感嘆自己實務經驗還不足,也許要再多看看 RoR 的設計概念了。
未留名 (#comment-4229555)
Tue, 20 Mar 2007 16:52:18 +0800
這牽涉到 PHP 的動態能力限制。
請看 MixableClass.php 的第31行,然後想想如何完成指派動作。
PHP 有三種呼叫方式: 一般函數、類別靜態方法、實例方法。基本上把第21行的 function_exists($fn) 改成 is_callable($fn) 就可以判定這是可呼叫的。但到第31行就麻煩了,如何儲存?

如果這三種方式都要考慮,這程式要增加不少程式碼。

現在的方式可以應付我的需求了,所以我目前只寫到這裡。更多內容要等到我有這需求時才會再去改了。

事實上,上面的作法無法同時兼顧效能與彈性。我覺得要改用 runkit extension 實現較佳。但 runkit 仍是新近加入的實驗性功能,還不是很穩固。
未留名 (#comment-4229805)
Tue, 20 Mar 2007 17:44:11 +0800
瞭解,如何儲存方法的確是 PHP 的弱項。沒有 closure 的支援, PHP 的動態性還是相當不足。這個問題有時間的話,我再來研究看看。

不過很我很有興趣的一點是,目前我所看過的 PHP 應用項目也很少使用這樣的方法;不曉得石頭成老大你所開發的項目是什麼,為什麼會有用到這種概念的想法?
未留名 (#comment-4229813)
Tue, 20 Mar 2007 17:45:16 +0800
抱歉,上面的 closure 本來要寫的是「核心語法」。
未留名 (#comment-4229975)
Tue, 20 Mar 2007 18:15:30 +0800
哈哈, 我都叫 closure 「封絕」或「閉鎖空間」的。XD

我目前有些功能直接用 Variable function 的方式,我在想如果能用 MixableClass 應該更方便。

目前還沒動手應用 MixableClass 於工作上。讓我想想要怎麼弄個實用的例子說明。
未留名 (#comment-4231255)
Tue, 20 Mar 2007 22:07:50 +0800
「封絕」...有點放武俠大絕的味道 XD

其實沒有實際用上也沒關係,我覺得這樣討論 PHP 可以做到什麼境界還滿有趣的。 :)

我已經想到一些可能的寫法了,這兩天有空來試試看。
未留名 (#comment-4235025)
Wed, 21 Mar 2007 11:54:59 +0800
看了mix-in這兩篇,第二篇覺得明確很多,這篇前段寫了目標,真的很棒。目標寫的好以及具體化,甚至比詳細的技術解決方案還重要,不過這是以應用來看就是了。

剛看的時候,其實還蠻不懂到底在玩什麼飛機,後來兩篇看完外加看了jaceju的那一篇和Prototype-based programming的解釋,才進入狀況,我實在是太蠢了。
未留名 (#comment-4241137)
Thu, 22 Mar 2007 10:58:53 +0800
「封絕」是一種自在式,可以實現一個與外部隔離的孤立空間,是紅世使徒... 停! 再講下去只有動漫狂聽得懂了。

其實像事件委派、序列化等動作都可以用混入的方式處理。例如,我要為一個類別加上可以將個體的公開內容序列化為 json 格式的方法時,就可以用混入而不是繼承方式處理。

例如:
User::delegate('User', 'jsonEncode', 'jsonEncode');
ProductOrder::delegate('ProductOrder', 'jsonEncode', 'jsonEncode');
Report::delegate('Report', 'json', 'jsonEncode');

這三個類別沒有親近的類別關係,若以繼承方式加上 json 序列化方法,可能要繼承好幾代。但以混成方式,只要直接委派就可以了。

另一種應用是和 Configuration-Driven Development 結合,自組態檔中讀出類別的schema後,即可動態混成新的類別,而不需要手動撰寫或產生程式碼。參見: Example of Configuration Driven Development with PHP從 XML 產生 JSON 資料及方法的封裝
未留名 (#comment-4243489)
Thu, 22 Mar 2007 18:14:15 +0800
嗯,看來應用還滿多的。以往如果要這樣實現的話,可能要多出好幾個類別。

也許還可以用在我目前發想的專案裡,這種方式實在是讓我學到了很多新想法 :)