Hack Your Design!

PHP5.4 から導入された trait 機能の使いドコロ

この記事はPHP Advent Calendar 2013の8日目の記事です。本エントリではphp5.4の注目機能の1つであるtraitをどうやって扱うべきかを書いてみようと思います。

Traitとは

Traitとは継承関係と関係なく実装を再利用できる仕組みのことです1。言い換えるならば、「多重継承」であったり「Mix-in」を可能にする仕組みと言えるでしょう。Rubyistの間ではMix-inの概念は当たり前のことでしょうが、PHP界隈ではTraitは新しい機能ということもあり浸透していない概念かと思います(Ruby以外にもTraitと似た機構はあるみたいですね2)。

本エントリではPHPerの間では未だ聞き慣れないであろうTraitの概念を実コードとともに紹介してみようと思います。

Traitの特徴

Traitの簡単な特徴は以下の通りです。

  • PHP5.4以降必須
  • Trait自身のインスタンスを作成することはできない
  • 振る舞いを継承関係のような上下方向ではなく水平方向に定義可能

Traitを使うと嬉しくなる状況

ではTraitを使うべき状況、使うと嬉しい状況とはどんな状況でしょうか? まずは下記を満たす実装を普通にPHPで書いてみます。

  • 人間(Human): 「話す」「逃げる」などの基本行動が可能
  • 戦士(Warrior): 基本行動に加え「物理攻撃」が可能
  • 魔法使い(Wizard): 基本行動に加え「魔法攻撃」が可能

結果、このようになりました。

<?php

class Human {
    function talk() {
        echo '話す';
    }

    function escape() {
        echo '逃げる';
    }

    /**
     * 使用可能なコマンド一覧を出力
     */
    static function commands() {
        $class = get_called_class();
        echo $class . ' can use ';
        foreach (get_class_methods($class) as $method) {
            echo "`{$method}` ";
        }
    }
}

class Warrior extends Human {
    function attack() {
        echo '攻撃';
    }
}

class Wizard extends Human {
    function spell() {
        echo '魔法';
    }
}

Warrior::commands(); //=> Warrior can use `attack` `talk` `escape` `commands`
Wizard::commands(); //=> Wizard can use `spell` `talk` `escape` `commands`

基本行動可能なHumanクラスがあってそれを継承したWarrior,Wizardがいて…。普通のコードかと思います。

追加要件:「魔法戦士を追加したい!」

こんな要望がきました。「魔法戦士も追加したいんだよねー!」 さて上記のような実装に魔法戦士を加えるとしたらどうなるでしょうか?

魔法も攻撃もできなきゃいけないから、まずWarriorを継承してその次にWizardも継承して…と、ここで多重継承の問題が出てきますね。愚直にHumanを継承した魔法戦士を作ってもいいのですが、同じコードを二度は書きたくないですよね。DRYに行きたいですよね、ね?

Traitを使ってみる

そこでTraitの出番です。traitであれば振る舞いを水平方向に構成できちゃうんです。え?よくわからない? コードを見てください。

<?php

trait Attackable {
    function attack() {
        echo '攻撃';
    }
}

trait Spellable {
    function spell() {
        echo '魔法';
    }
}

class Human {
    function talk() {
        echo '話す';
    }

    function escape() {
        echo '逃げる';
    }

    /**
     * 使用可能なコマンド一覧を出力
     */
    static function commands() {
        $class = get_called_class();
        echo $class . ' can use ';
        foreach (get_class_methods($class) as $method) {
            echo "`{$method}` ";
        }
    }
}

class WarriorWizard extends Human {
    use Attackable, Spellable;
}

WarriorWizard::commands(); //=> WarriorWizard can use `talk` `escape` `commands` `attack` `spell`

$ww = new WarriorWizard();
$ww->spell(); //=> 魔法
$ww->attack(); //=> 攻撃

まずはAttackableSpellableというトレイト、つまり「物理攻撃が可能である」「魔法攻撃が可能である」という特性を定義します。ここまでくればもう簡単。あとはそれをWarriorWizardクラス内でuseしてやるだけです。これでWarriorWizardは魔法も物理攻撃も可能になります。

ね?簡単でしょ?

今後のTrait事情―まだ来ないであろうTraitの波 

今回紹介したTraitはフレームワークの設計を根本から変えうる素晴らしい機能なのですが、PHP5.4の使用率がまだ10%なこと、まだまだTraitを使ったコード使用例が少ないこと、これらを考えるとTraitがPHPerに広く普及するにはあと2,3年はかかるのかな、と考えています。

CakePHP3では、PHP5.4が必須となり、trait機能がうまく活用された設計となっています。このフレームワークレベルでのTrait有効活用を機に、Traitを利用したナイスなコード、ひいてはPHP5.4の使用が今後増えていけばいいなと思っています。

  1. Trait とは? その使い道を考えてみる 

  2. RubyだとMix-in、Scalaだとtrait、Perl6だとrole にあたります。 

  • このエントリーをはてなブックマークに追加