読者です 読者をやめる 読者になる 読者になる

PHPで動的継承

PHP

Rubyにある特異クラスや特異メソッドの便利さを、PHPでも実現してみようと試みました。Rubyの特異クラスは、すでにインスタンス化された、クラス不明なオブジェクトに、オーバーライドでメソッドを追加するものです。

特異クラスはスクリプト言語ならではの機能です。たとえば、フレームワークの早い段階でインスタンス化されたオブジェクトが、後半のユーザコードに適合しない場合を考えましょう。フルのラッパークラスを定義してアダプターパターンを適用してしまいそうですが、特異クラスでオブジェクト自体のメソッドを追加・上書きすれば、インターフェースを合わせることができ、面倒なラッパーも要りません。以降、拡張済みオブジェクトはオリジナルオブジェクトのように使えます。

JavascriptでもPythonでも、相当のことは簡単にできます。

var obj = new Object();
obj.extraMethod = function(){ ...; }
obj.extraMethod();
obj = object()
obj.extraMethod = lambda self:...
obj.extraMethod()

さて、文字列をいちいちパースしているくせに、言語仕様はJavaC++に近いという変な言語、PHPではどうでしょう?

現在のPHPには、特殊なメソッド名がいくつかあり、変数やメソッドがなかった場合のフェイルセーフが考慮されています。
http://php.net/manual/ja/language.oop5.overloading.php

この特殊メソッド名を使って、拡張前のオブジェクトインスタンスをターゲットとして、変数操作やメソッド呼び出しをすべて委譲し、そのうえで、特異メソッド化したいメソッドを自分のクラスに定義すると、ダイナミックなオブジェクト変形ができそうです。

<?php
/**
 * 委譲の基本オブジェクト
 */
class DelegateBase {
    private $_parent;
    
    public __construct($parent)
    {
        $this->_parent = $parent;
    }

    public function __get($name)
    {
        return $this->_parent->$name;
    }
    
    public function __set($name, $value)
    {
        $this->_parent->$name=$value;
    }
    
    public function __isset($name)
    {
        return isset($this->_parent->$name);
    }
    
    public function __unset($name)
    {
        unset($this->_parent->$name);
    }
    
    function __call($name, $args)
    {
        return call_user_func_array(array($this->_parent, $name), $args);
    }
}

// Mix-inはないので、単純に継承して使うしか…
class SpecialClass extends DelegateBase {
    function foo()
    {
        return "extended " . $this->_parent->foo();
    }

    function baz()
    {
        return $this->bar() . " via baz"; // barは_parentに委譲
    }
}

// $existing_obj はメソッド foo と bar を持つ既存のインスタンス

$myobj = new SpecialClass($existing_obj);

echo $existing_obj->foo(); // "foo"
echo $existing_obj->bar(); // "bar"
echo $existing_obj->baz(); // エラー

echo $myobj->foo();        // "extended foo"
echo $myobj->bar();        // "bar"
echo $myobj->baz();        // "bar via baz"

//その他、$existing_obj が持っているメソッドと変数はすべて、$myobj からそのまま使える
?>

こんな感じになりました。これ自体の動作を確認してはいませんが、等価な実装でうまく動いてるので、たぶん大丈夫。かたちとしては、自動的なラッパーになったわけですが、そこそこ目的は達成できました。

これで、擬似派生クラスのほう(SpecialClass)の実装コードをパースするのを、遅延させることができます。もしかしたら派生の必要がないかもしれないので、静的派生のように、毎回派生クラスをパースして負荷をかけるのはばかばかしいです。

でも要注意!! 呼び出しがいったん基底オブジェクトに入ってしまうと、いくら派生側のクラスでオーバーライドしていても、基底オブジェクトのメソッドが呼ばれてしまいます。正しく「継承」ではありません。という意味で、「デリゲート」と名づけています。

やっぱり、動的なスクリプト言語に、静的なクラス継承は似合いませんね。

ここから派生し、「値がなかったら委譲先を分岐させる」などして、ミックスインもどきを作ることもできそうです。まあ、いったん委譲してしまうと帰ってこないから、オブジェクトを混ぜても、せいぜい、「入り口が同じインスタンスだ」というぐらいの意味にしかならないでしょうけど。