PHP 實踐 mix-in 概念 part 2 - MixableClass
第一部份《PHP 實踐 mix-in 概念之可行性》一文中解釋了 PHP 的個體如何加入新的方法。但那僅針對個體而非類別,那些混成內容無法繼承再用。而 Ruby 的混成(mix-in)概念是針對類別,其混成結果是一個類別,這些混成內容可經繼承機制再用。所以我接下來就要為 PHP 實踐一個可以混成的類別 - MixableClass
。
我的設計目標有二。第一、個體可以動態增刪方法,且不影嚮其他個體。第二、以抽象化方法混成新的類別。
個體可以動態增刪方法
我可以將某類別的實例視為獨立個體,僅為此個體增添方法,而不經類別關係影嚮其他同類或衍生類別之個體。舉例而言,當我配置了個體 $x 之後,我可以只為 $x 增加 foo 方法。而其他個體不論是否與 $x 同類,皆不會自動具有 foo 方法。這一點在第一部份《PHP 實踐 mix-in 概念之可行性》已經實現了。
以抽象化方法混成新的類別
可用「與特定類別無關的抽象化方法」混成新的類別,且混成類別的特徵仍然要與一般類別相同。
- 混成基礎類別的方法可為衍生類別所繼承。
若我以 foo 方法混成了 MyClass 類,則繼承 MyClass 類之 MyClass2 類也會具有 foo 方法。 - 這些混成類別可以隨時增刪方法,且動態增刪之方法亦須依繼承原則運作。
若我將 foobar 方法動態加入 MyClass2 類,不僅所有已配置之 MyClass2 實例將立即具有 foobar 方法,連其衍生類別 MyClass3 之實例也將依繼承原則而具有 foobar 方法。 - 遵循子承父、父不承子之繼承原則。
當我動態加入 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 的概念」。他們的設計目標與我略有不同,可兩邊比較設計內容。
樂多舊回應