コラム

trait トレイトは便利な道具箱

※この記事の注意事項
・できるだけ初心者の方が理解しやすいように、細かい説明は省略しております。
・最後まで読み進められるように、多少ユーモアを交えて表現していることがあります。

========================================

trait トレイトはPHPにとって、割と新しい文化(PHP 5.4から導入)なんだけど、便利な反面上手に使わないとわけわからなくなるよって思う。

特に、よく考えないと理解できないコードというものは事故が起こりやすいです。
ですので、直感で理解できるようなコードを書くことが保守性の高い優秀なコードになると思っているよ。

trait トレイトとはなにか?

トレイトとは、抽象クラス(abstruct)とインターフェイス(interface)のいいとこ取りしたようなもので、トレイトには継承のような縦の関係はなく、機能をまとめたものであるので、言ってしまえば道具箱のようなものですね。

トレイトもインスタンスを作成することはできません
トレイトもpublicしかないんだけど、クラスで使用する時にアクセス権は変更できるよ。
トレイトを使用する場合はuse文を使って呼び出します。

trait HelloWorld {
    public function sayHello() {
        echo 'Hello World!';
    }
}

class MyClass1 {
    //普通にトレイトを使用
    use HelloWorld;
}


class MyClass2 {
    //sayHelloの可視性を変更します
    use HelloWorld { sayHello as protected; }

    //protectedにしたので、クラスの外から呼べるメソッドを用意
    public function say() {
        $this->sayHello();
    }
}

class MyClass3 {
    //sayHelloのエイリアスを作成し、可視性を変更します(sayHello自体の可視性は変わらない)
    use HelloWorld { sayHello as private myPrivateHello; }

    public function say() {
        $this->myPrivateHello();
    }
}

$instance1 = new MyClass1();
$instance1->sayHello();

$instance2 = new MyClass2();
$instance2->say();

$instance3 = new MyClass3();
$instance3->say();
複数のトレイトを設定することは、あちこちの道具箱から集めてくることと似ている

トレイトは複数持つこともできるよ。

例をあげると、
医療セットのトレイトと、工具セットのトレイトがあって、
親クラスキャンプセットメソッドを持っているとします。
長男の子ども1は、親を継承してキャンプセットメソッドを持ち、かつ、医療セットトレイトを持ち、おもちゃセットメソッドも自分で持っています。
次男の子ども2は、自分で独自に持っているものはないんだけど、医療セットトレイトと、工具セットトレイトを持っているとします。

コードにするとこんな感じになります。

trait iryoSet {

    public function bansoukou() {
        echo 'ばんそうこう';
    }

    public function shoudokueki() {
        echo '消毒液';
    }

    //使用可能なコマンド一覧を出力
    public static function commands() {
        echo "class => ".get_called_class()."\n";
        var_dump(get_class_methods(get_called_class()));
    }
}

trait kouguSet {
    public function tonkachi() {
        echo 'トンカチ';
    }
}

class oya {
    public function campSet() {
        echo 'キャンプセット';
    }
}

class kodomo1 extends oya {
    use iryoSet;

    public function omochaSet() {
        echo 'おもちゃセット';
    }
}

class kodomo2 {
    use iryoSet, kouguSet;
}

$instance1 = new kodomo1();
$instance1->commands();

$instance2 = new kodomo2();
$instance2->commands();
あちこちの道具箱から同じ名前の道具を持ってきた場合は、どの道具を使うのか指定してあげる必要がある

トレイトは複数持てる分、名前の衝突が起こる可能性がありますよね。
そこで衝突の解決を行う必要があるんです。

例えば、
電子機械トレイトと、大工トレイトがあって、どちらにもトンカチがある場合です。

trait Denshikikai {
    public function tonkachi() {
        echo 'Tonkachi';
    }
    public function seimitsudriver() {
        echo 'Seimitsudriver';
    }
}

trait Daiku {
    public function tonkachi() {
        echo 'BigTonkachi';
    }
    public function nokogiri() {
        echo 'Nokogiri';
    }
}

class Me {
    use Denshikikai, Daiku {
        Daiku::tonkachi insteadof Denshikikai;
    }
}

$instance = new Me();
$instance->tonkachi();

クラスMeではどっちのトンカチを使っていいか悩んでしまうので、ここでは大工さんの持っているビッグトンカチを使うことにしました。

メソッドの上書き優先順位に注意

あと、トレイトで大事なことに、継承の場合の優先順位があります。

クラスの特性で、同じ名前のメソッドは上書きされるんだけど、トレイトもその特性は同じで、大事なのは優先順位なんだ。

優先順位は、子メソッド > トレイトのメソッド > 親メソッドになるよ。

以下のコードの場合、子メソッドでも、トレイトのメソッドでも、親のメソッドでもsayHelloメソッドを持っているので、子メソッドが優先されているよ。

class Oya {
    public function sayHello() {
        echo 'OyaHello ';
    }
}

trait SayWorld {
    public function sayHello() {
        echo 'TraitHello';
    }
}

class Kodomo extends Oya {
    use SayWorld;

    public function sayHello() {
        echo 'KodomoHello';
    }
}

$instance = new Kodomo();
$instance->sayHello(); //KodomoHello

それで、以下のコードの場合は、トレイトのメソッドと親のメソッドでsayHelloメソッドを持っているので、トレイトメソッドが優先されているんだよね。

class Oya {
    public function sayHello() {
        echo 'OyaHello ';
    }
}

trait SayWorld {
    public function sayHello() {
        echo 'TraitHello';
    }
}

class Kodomo extends Oya {
    use SayWorld;
}

$instance = new Kodomo();
$instance->sayHello(); //TraitHello
トレイトでメンバ変数を設定したら、クラスでは同じ名前のメンバ変数は設定できないよ

トレイトでは、abstructなどの抽象化メソッドを使って、実装を強制することはできるんですけど、メンバ変数に関しては注意が必要です。

トレイトで設定したメンバ変数を、クラスで設定することはできないんだ。

以下のコードでは、トレイトでhello変数を設定しているので、クラスでhello変数を設定するとエラーになります。

trait Say {
    public $hello = 'TraitHello';

    public function sayHello() {
        echo $this->hello;
    }

    abstract public function sayGoodBaye();
}

class Kodomo {
    use Say;

    public $hello = 'KodomoHello'; // error

    public function sayGoodBaye() {
        echo 'Matane!';
    }
}

$instance = new Kodomo();
$instance->sayHello();
$instance->sayGoodBaye();
まとめですが、

まとめというより雑感ですが、トレイトはコードの再利用や共有するためにあると思います。そしてトレイトは柔軟でクラスとほぼ同じような機能があります。
でも便利な反面、きれいにまとめないといったいどれが優先されて表示されているのかわからないなんてことにもなり兼ねません。

結局は使う人しだいなんですが、道具に溺れないようにしっかりと考え方を持って使うべきだと思いますね。
それはabstructやinterfaceも同じですが。
保守に優しいコードを書きたいものです。

========================================

▼PHPのオブジェクト指向とデザインパターン記事一覧

関連記事

コメント

  • トラックバックは利用できません。

  • コメント (0)

  1. この記事へのコメントはありません。