PHP5.4のtraitを使ったシングルトンパターン実装によるtrait入門

PHP5.4 alpha1がリリースされた。このリリースでは、PHPオブジェクト指向言語の新たな機能としてtraitと呼ばれる機能が追加された。PHP5.4におけるtraitとは、型に影響を与えずにクラスに適用できるメソッドとプロパティの集合である。

早速PHP5.4 alpha1をインストールし、traitを使ってシングルトンパターンを実装した。このコードでは、クラスの継承関係に影響を与えずにシングルトンパターンをモジュール化している。

<?php

trait Singleton
{
    protected function __construct() { }
    static function getInstance()
    {
        static $obj = null;
        return $obj ?: $obj = new static;
    }

    function __clone()
    {
        throw new RuntimeException("You can't clone this instance.");
    }
}

クラスの継承関係に影響を与えないとはどういうことか? traitは、それを使うクラスがどのクラスを継承していてもどのインターフェイスを実装していても関係なく利用できる。例えば、上で書いたtraitは以下のように使える。

<?php

class MyBaseClass
{
    function doSomething()
    {
        /* ... */
    }
}

class MyClass extends MyBaseClass
{
    use Singleton; // Singleton traitを使用する

    function doMySomething()
    {
        /* ... */
    }
}

$pro = MyClass::getInstance();
$pro->doSomething();
$pro->doMySomething();

new MyClass; // コンストラクタを直接呼び出すとエラー

getInstanceというメソッド名が気に入らないなら、以下のようにasキーワードを用いてエイリアスを設定することも出来る。

<?php
class MyHoge
{
    use Singleton
    {
        getInstance as singleton;
    }
}
$hoge = MyHoge::singleton();

また、シングルトンなオブジェクトを保持したいが、コンストラクタからインスタンス化したい、という場合は以下のようにasキーワードを用いてメソッド属性の変更を行う。さらにcloneを禁止したくない場合は単純にtraitのメソッドをオーバーライドする。

<?php
class MyHoge
{
    use Singleton
    {
        __construct as public; // コンストラクタをpublicにする
    }

    function __clone() { } // cloneした際に例外を出さないようにオーバーライド
}

$hoge = new MyHoge;    // エラーは出ない
$hoge2 = clone($hoge); // cloneした際も例外は出ない

traitは型に影響を与えずにクラスが利用出来るメソッドやプロパティの集合である、と書いた。これはどういう事なのか。

今回追加されたtraitの立ち位置を言う前に、まずPHPに以前から実装されているオブジェクト指向言語の機能を整理する。

クラス型のオブジェクト指向における継承では、ある親クラスを継承した子クラスは親クラスのメソッドやプロパティなどのメンバを再利用出来る。それだけではなく、子クラスの型は親クラスの型としてもみなせる。以下のように。

<?php

class Hoge
{
    function doHoge()
    {
        echo "hoge";
    }
}

class Fuga extends Hoge
{
    function doFuga()
    {
        echo "fuga";
    }
}

$fuga = new Fuga;
$fuga->doHoge(); // 親のHogeクラスのメソッドが利用出来る
$fuga->doFuga();

var_dump($fuga instanceof Hoge)); // Hogeオブジェクトとしてもみなせる

call_user_func(function(Hoge $hoge) { // タイプヒントでもエラーは出ない
    /* ... */
}, $fuga);

オブジェクト指向言語のなかには、複数の親クラスを継承できる言語もあるがPHPはそうではない。従ってクラスを使ってSingletonパターンをモジュール化しようとしても、何か親クラスを持っているクラスはこれを使うことができない。クラスの継承は、多くのケースで十分に強力だがその一方で不便なケースも存在する。

<?php
abstract class Singleton
{
    protected function __construct() { }
    static function getInstance()
    {
        static $obj = null;
        return $obj ?: $obj = new static;
    }
}

class Hoge extends Fuga
{
    // このクラスはすでにFugaクラスを継承しており、上のSingletonクラスを継承できない…
}

インターフェイスでは、そのインターフェイスに定義されているメソッドを実装クラスがその役割どおり実装することで、その実装クラスの型をインターフェイスの型としてみなすことが出来る。クラスはインターフェイスをいくつも実装できる。

<?php

interface Hoge
{
    function hoge();
}

interface Fuga
{
    function fuga();
}

class Piyo implements Hoge, Fuga
{
    function hoge()
    {
        echo "hoge";
    }

    function fuga()
    {
        echo "fuga";
    }
}

$piyo = new Piyo;
var_dump($piyo instanceof Hoge); // => true
var_dump($piyo instanceof Fuga); // => true

「継承」では親クラスのメソッドやプロパティなどのメンバを再利用でき、かつ親クラスと同じ型として見なせる。それに対して「インターフェイスの実装」では、実装したインターフェイスとクラスを同じ型として見なせる。また、一つのクラスは複数インターフェイスを実装できる。「継承」と「インターフェイスの実装」が違うのは、インターフェイスが持っているのはメソッドシグネチャのみなのでそこにコードの再利用はない点だ。

では、traitは?

<?php

trait Hoge
{
    function doHoge()
    {
        echo "hoge";
    }
}

trait Fuga
{
    function doFuga()
    {
        echo "fuga";
    }
}

class Piyo
{
    use Hoge, Fuga;
}

$piyo = new Piyo;
$piyo->doHoge(); // "hoge"と表示される

var_dump($piyo instanceof Hoge); // => false

traitはインターフェイスの様にクラスからいくつでも利用できるが、traitは型を持たない。従ってクラスの型に影響を与えない。

ここまでの話を簡単に表にまとめると以下のようになる。

コードの再利用 型に影響 複数適用できるか
継承 ×
インターフェイス ×
trait ×

継承は、親クラスのコードを再利用し、型に関しても親クラスの型として見なせる。クラスが継承できるのはひとつのクラスだけである。 インターフェイスは、メソッドの中身を持たないのでコードの再利用はないが、クラスにいくつも型を結びつけられる。traitは、クラスの型になんの影響も与えないが、メソッドとプロパティの集合をクラスの中にいくつも取り込むことが出来る。

というわけでPHP5.4 alpha1から新しく登場したtraitの基本的な概念について説明した。

次は、traitのメソッドの衝突とそれの解決について書く。以下のように、利用する複数のtraitでメソッド名が重複した場合、クラスの宣言時にエラーを出す。これをメソッドの衝突と呼ぶ。

<?php

trait A
{
    function doSomething()
    {
        echo "hoge";
    }
}

trait B
{
    function doSomething()
    {
        echo "fuga";
    }
}

class C
{
    use A, B; // fatal error!
}

メソッドの衝突を解決するには、新しく導入された"insteadof"キーワードを用いて優先度を設定する。

<?php

class D
{
    use A, B 
    {
        A::doSomething insteadof B; // A::doSomethingメソッドを優先する
    }
}
$d = new D;
$d->doSomething(); // "hoge"と表示される

衝突するメソッドの両方を利用したい場合は、"as"キーワードを用いてエイリアスを設定してメソッドの衝突を解決する。

<?php

class E
{
    use A, B
    {
        A::doSomething as doSomethingOnA;
        B::doSomething insteadof A;
    }
}
$e = new E;
$e->doSomething();    // "fuga"と表示される
$e->doSomethingOnA(); // "hoge"と表示される

勘違いしやすいが、"as"キーワードを用いたメソッドエイリアス設定は、メソッド名の変更ではないので注意する。以下のような例を見るとそれがわかる。

<?php
trait A
{
    function doSomething()
    {
        echo "hoge";
    }
}

class B
{
    use A
    {
        doSomething as hoge;
    }
}

$b = new B;
$b->doSomething(); // "hoge"と表示される
$b->hoge();        // "hoge"と表示される

メソッド名の変更ではなくメソッド名のエイリアスが作成できる理由は、あるtraitのメソッド内で同じtraitのメソッドを呼び出している場合に、うかつにメソッド名を変えられるとコードが破壊されてしまうようなことを防ぐためと考えられる。また、メソッドの衝突を解決した際にもtrait本来の振る舞いが破壊されないように考えられていることが以下の例を見るとわかる。

<?php
trait A
{
    function doSomethingTwice()
    {
        $this->doSomething();
        $this->doSomething();
    }

    function doSomething()
    {
        echo "hoge";
    }
}

trait B
{
    function doSomething()
    {
        echo "foobar";
    }
}

class C
{
    use A, B
    {
        A::doSomething as doSomethingOnA; // エイリアスを指定する
        B::doSomething insteadof A;               // 優先度を設定する
    }
}

$c = new C;
$c->doSomethingOnA();   // "hoge"と表示される
$c->doSomething();      // "foobar"と表示される
$c->doSomethingTwice(); // "hogehoge"と表示される

こういった例を見ると、新たに登場したtraitがメソッドの衝突の回避やそれを単純に回避した際に起こる問題にもきちんと考えられているのがわかる。

まとめ

  • traitは、クラスに適用できるメソッドとプロパティの集合である
  • traitの適用は、そのクラスの型に影響しない
  • クラスの継承とインターフェイスの実装だけでは困るケースが存在する
  • traitメソッドの衝突を解決するには、優先順位を設定する

この記事ではPHP5.4 alpha1で登場したtraitについてひと通り記述した。traitを触ってみた感想としては、traitを導入した際に付いてくる問題を解決する方法が予め用意されており、考えなしに実装されたアドホックな機能ではないという印象を受けた。PHP5.4正式リリースが楽しみだと思った。まる。

追記(7/9)

この記事中で、メソッドの衝突を解決するのにinsteadofを使わずにasのみを用いているサンプルコードがありましたが、それは間違いですので修正しました。
指摘してくださったid:sumimさんありがとうございます。