trait時代のライブラリ設計を予想してみる #php5_4

Posted by Hiraku on 2011-12-23

この記事はPHP5.4 Advent Calendar 2011の23日分です。

trait自体はすでに10日目の@tpyamamotoさんによるTRAITでデザインパターン再考でも取り上げられていますが、目玉機能なのでもう一回ネタにしちゃいます><

おさらい:traitとは

相変わらず公式マニュアル充実しまくっているのでそちらを見た方がいいのですが、抜粋するとこんな感じ。

  • PHP5.4の目玉機能
  • 多重継承を安全に行えるよう導入された概念
  • traitはプロパティ・メソッドを持てる
  • traitは定数を持てない
  • trait自体はインスタンス化できない
  • classを継承してtraitを作ることはできない
  • interfaceを実装してtraitを作ることはできない
  • traitを継承してtraitを作ることが可能
  • classは複数のtraitを継承して実装を再利用できる
  • traitはclassではないのでタイプヒントに使えない

traitが単なるクラスの多重継承より安全なのは、名前衝突時に別名をつけられたり、そもそもクラスと別概念なのでタイプヒントに悪影響を及ぼさなかったりといった特徴によるものです。

多重継承が解禁されたことにより、フレームワークの設計を根幹から変えてしまう可能性を秘めています。

traitを使った行儀のよい書き方例

traitはインターフェースの代わりにならないので、インターフェースをきっちり定義しておきます。
interface Greetable {
  const hello = 'Hello';
  function sayHello();
  function sayWorld();
}
そのうえでtraitを定義していき、
trait SayHello {
  function sayHello() {
    echo Greetable::hello;
  }
}

trait SayWorld {
  function sayWorld() {
    echo 'World';
  }
}
これらをくっつけてクラスを作ります。
class Hoge implements Greetable {
  use SayHello, SayWorld;
}
クラスの型はインターフェースで判定します。
function foo(Greetable $greet) {
  $greet->sayHello();
  $greet->sayWorld();
}
と、こんな感じがtraitの上品な使い方だと思います。

具体的に多重継承が許可されたことにより設計はどう変わるのか?を考えてみます。

予想) mixin再現ライブラリが必要なくなる

委譲とマジックメソッドを駆使することで、既存のPHPでも多重継承を再現するライブラリというのがいくつか存在していましたが、言語公式に多重継承がサポートされたことでこれらは必要ないものになりました。

フレームワークも、よりシンプルな記述で柔軟なものを作れるようになるでしょう。

マイクロフレームワークが活性化するかも?

予想) 抽象クラスがオワコン化する?

割と拡張性を気にしているライブラリの場合は、今まで3層になっていることが多かったと思います。

  • Zend_View_Interface。interfaceでありクラスの仕様書。タイプヒントにはこれを使用する
  • Zend_View_Abstract。interfaceに「たいてい使うだろう」って機能を実装した抽象クラス
  • Zend_View。 abstractを継承した具象クラス。標準ではこれを使う

まるっと差し替えたい場合はinterfaceから、ちょっと拡張するだけならabstractから、という具合に拡張の規模によって基底を選択でき、とても柔軟でよいのですが、abstractがクラスなのが難点でした。

例えばZend FrameworkにSmartyを組み込みたい場合、Smartyのクラスを基底にするか、Zend_View_Abstractを基底にするか悩まないといけません。そして基底にしなかった方のクラスは、長々と委譲のコードでつなぎ合わせないといけませんでした。

冒頭に列挙した性質を読んでいると、抽象クラスとtraitは似た概念であることがわかります。今まで仕方なく抽象クラスにしていたなら、traitに差し替えることで多重継承可能になって、拡張性が上がりみんな幸せになれると思います。

そのうち、「抽象クラスだと…?この俺に切り札である『クラス継承』を使わせようというのか!」「抽象クラスを使っていいのはPHP5.3までだよねーwww」みたいな空気感になったりするかも?

interfaceとtraitを二つ定義するのが面倒くさい、そこまで拡張性を担保する必要がない、コード量が増えるし遅くなったらいやだ、みたいな場合にのみ、抽象クラスは割り切って使われるようになるんではないでしょうか。

予想) 無尽蔵にメソッドを増やせることを前提にした設計が可能に

今まではメソッドを増やすには単一継承して手で書いていくほかなく、「メソッドを増やす」ことを前提とした設計はいまいちなものでした。これが多重継承によってやりやすくなります。

たとえば、PHPUnitのような設計はどうでしょう? PHPUnitは「test」から始まるメソッドをテストとみなして自動で実行するというTemplate Methodの派生みたいな設計になっていますが、こういうのが使いやすくなります。

//疑似コードですが。

class IndexController implements ActionController {
  use PreHookSupport, PostHookSupport;

  use PostHookAnalytics; //postHookでアクセス解析のコードを吐くセット
  //useでフックメソッドをどんどん増やせる

  function indexAction($req, $res) {
    $this->preHook();    //preHook〜で始まるメソッドを全部実行する

    // ...

    $this->postHook();   //postHook〜で始まるメソッドを全部実行する
  }

  function postHookHogehoge() {
    //actionの後にごにょごにょ処理する1
  }

  function postHookFugafuga() {
    //actionの後にごにょごにょ処理する2
  }
}

ただのTemplate Methodがまるで洗練されたプラグイン機構レベルの拡張性を持ち始めます。

もちろんObserverパターンとかにした方がいいんですけど、こんな手抜きの構造でも拡張性が担保できるってのはいいことではないでしょうか?


まとめ

  • traitによる多重継承で夢がひろがりんぐ
  • 各ライブラリの作者様は、ひとまずAbstractクラスをtraitに置き換えていくところから対応するといいかも

そのうち固まってくるだろうモノ

  • traitを使ったデザインパターン定石
  • traitの命名規則どうするの
  • 退避用の一時メソッドの命名規則どうするの(補足参照)

うーん、PHP5.4が楽しみですね。


補足:traitを継承する書き方

少し話がそれますが、使ってみたところ若干わかりにくいと思ったのが継承の書き方です。

クラスを継承する場合は、オーバーライド前の親クラスのメソッドを呼ぶにはparent::で呼ぶことになっていました。

class ChildKlass extends ParentKlass
{
  public function doSomething() {
    parent::doSomething();
    //適当に追加処理を書いたり
  }
}

これに対して、traitをuseするときは、複数useできてしまうので親が一意に決まってくれません。そのためparent::のようなキーワードで元の実装を呼び出すことはできません。以下の書き方は意図通りに動いてくれないのです。

class ChildKlass {
  use SomeTrait;

  public function doSomething() {
    SomeTrait::doSomething(); //この書き方は間違い!
    parent::doSomething();    //これも間違い!
  }
}

元の実装を呼び出したい場合、別名で退避させておく必要があります。その際、無意味にpublicメソッドが増えないよう、privateに差し替えておくとベターですかね。

class ChildKlass {
  use SomeTrait {
    doSomething as private _SomeTrait_doSomething;
  }

  public function doSomething() {
    $this->_SomeTrait_doSomething();
  }
}

若干面倒ですが、traitの性質を考えると妥当かもしれません。



keyword: PHP

PHPの最新記事

×

この広告は180日以上新しい記事の投稿がないブログに表示されております。