PHP 5.3でクラスにメソッドを動的に追加する

todaPHP

システムの規模の肥大化に伴うクラス数増大により、あるクラスにメソッド追加が集中することがあります。基盤となるクラスのメソッドが増大していくものです。
クラスを追加するときに、その関連メソッドを基盤クラス側のメソッドとして追加していくとそのような事態に陥りがちです。いわゆる、Blob(肥満児)アンチパターンですね。

あまり上手くない例えですが、ある会員情報を保存している MemberTable クラスに対して、その会員情報を操作するフォームを表す Form クラスが追加されていることがある (1:N) とします。

その MemberTable に所属している Form の一覧を取得する getForms() メソッドを MemberTable クラスに追加したいわけですが、静的にメソッドを追加した場合、Form クラスが使われない場合は無駄なメソッドが追加されることになります。

Form クラスだけならまだしも、このようなクラスが増えていくと、常に使用されるとは限らないメソッドが MemberTable クラスに追加されていくことになり、MemberTableのコードが無駄に肥大化することになります。また、 Form クラスの修正の影響が MemberTable クラスに及ぶことがありえるため、修正すべき箇所が複数のコードに分散してしまい保守性の観点からも望ましくはありません。

この問題の一つの解決手段として、メソッドを(必要なときに)動的に追加することで、静的に定義されるメソッド数を減らすという方法があります。
Form クラスが読み込まれた時に、 Form クラスのコード側で MemberTable クラスに getForms() メソッドを追加することができれば、MemberTable クラスのソースに手を加える必要はないので、MemberTable の肥大化を防ぐことができますし、 Form クラスの修正を Form クラスのコードに抑えることができます。

具体的な実装例としては、動的に追加するメソッドを無名関数として登録しておき、基盤クラスの __call() マジックメソッドを経由して呼び出すという方法があります。

class MemberTable {
  // 動的に追加されたメソッドの配列
  static private $_methods = array();

  // 動的にメソッドを追加
  static public function addMethod($name, $func) {
    self::$_methods[$name] = $func;
  }

  // 動的に追加されたメソッドの呼び出し
  public function __call($name, $args)
  {
    $func = self::$_methods[$name];

    // 無名関数に $this を bind してから実行
    call_user_func_array($func->bindTo($this, 'MemberTable'), $args);
  }
}

Form クラス側のコードでは、読み込み時に MemberTable::addMethod() でメソッドを追加します。

class Form {
  // 詳細略
}

// MemberTable クラスに getForms() メソッドを追加
MemberTable::addMethod("getForms", function(){
  // 詳細略
  return $forms;
});

Form クラスが読み込まれるときに、MemberTable::addMethod() でメソッドを追加されているので、何も気にせずに getForms() を呼び出すことができます。

$forms = (new MemberTable())->getForms();

ポイントはメソッドの呼び出し時に、bindTo() で無名関数の $this を bind しておくことです。これを忘れると、無名関数内で $this を参照できません。
あと、bindTo() するときにスコープ(第2引数)にクラス名を指定しておく必要があることをお忘れなく。

しかし、bindTo() が使えるのは PHP 5.4 以降であって、公式パッケージが未だに PHP 5.3 である RedHat/CentOS では、無名関数を $this に bind することはできません。

そこで無名関数の呼び出し時に、引数に $this を暗黙的に追加して、無名関数側の引数として受け取るという方法があります。

class MemberTable {
  public function __call($name, $args)
  {
    // 引数の先頭に $this を追加
    array_unshift($args, $this);

    return call_user_func_array(self::$this->_methods[$name], $args);
  }
}

無名関数を登録するときは、最初の引数で $this を受け取るようにします。
クラスのメソッドは $self 経由で呼び出します。

class::addMethod('method', funciton($self, $arg1, $arg2..){
  // 詳細略
  return $forms;

  // メソッドを呼び出すときは $self から呼び出す
  $self->someMethod();
});

これである程度同様のことが実現できます。
もちろん制約もあって、$self は自身の private, protected メソッドを呼び出すことはできません。

早く RedHat/CentOS の公式パッケージが PHP 5.4 ベースになってくれることを期待したいところです。

todaPHP

Posted by toda