プロぽこ

【解説】PHPのtrait(トレイト)が便利!プログラムの再利用がはかどる

phpのtraitという機能をご存知でしょうか?traitはphp5.4で追加された機能で、プログラムを再利用するための仕組みの1つです。

traitは単なる機能だけを保持するもので、クラスの継承関係とは別軸で存在します。必要な時に必要な機能を持ったtraitをクラスに組み込んでその機能を使うことになります。

この記事ではtraitの簡単な使い方をできるだけ丁寧に解説していきたいと思います。traitを知らない方も多いと思いますので、理解する際の助けとなれば幸いです。

PHPのtrait(トレイト)とは

下記の2点がtraitの特徴です。
1. メソッド及びプロパティを定義できる
2. traitからインスタンスを作ることはできない

抽象クラスに似ていますが、冒頭でも述べたようにtraitは機能だけを持ち、クラスの継承関係とは別のものです。

PHPの公式リファレンスにもありますが、クラス間の継承関係が垂直方向での振る舞いの構成とすると、traitは水平方向での振る舞いを構成するものです。

下記にとても簡単な例を出してみます。traitを使ったメソッドの組み込みです。クラスでは組み込みたいtraitをuse文を使って指定します。

するとそのクラスのインスタンスはあたかもそのクラスに定義されているかのようにそのメソッドを使用することができます。


trait MyTrait
{
  function hello()
  {
    echo 'hello'.PHP_EOL;
  }
}

class MyClass
{
  use MyTrait;
}

$class = new MyClass();
$class->hello(); // helloと表示される

とは言え、「こういうことをしたいならやっぱり継承じゃダメなの?」という疑問が浮かぶと思います。繰り返しになりますが、traitは今までの「継承」という機能に取って代わるものではありません。

通常ある機能「A」を他のクラスで使いたい場合、そのクラスを継承するか、機能「A」を持った親クラスを定義し、そちらを継承するかという方法をとることになります。

下記はRPGでありがちな設計の例です。抽象クラスHumanを継承した各職業Archar,Lancer等が存在している様子です。至って普通だと思います。


bstract class Human
{
  private $name;
  public function __construct($name = 'myname') { $this->name = $name; }
  public function getName() { return $this->name; }
  abstract public function getJob();
  abstract public function getWeapon();
}

class Archar extends Human
{
  public function getJob() { return 'Archar'; }
  public function getWeapon() { return 'bow'; }
}

class Lancer extends Human
{
  public function getJob() { return 'Lancer'; }
  public function getWeapon() { return 'lance'; }
}

$archar = new Archar('sea');
echo $archar->getName() . PHP_EOL; // sea
echo $archar->getJob() . PHP_EOL; // Archar

$lancer = new Lancer('mount');
echo $lancer->getName() . PHP_EOL; // mount
echo $lancer->getJob() . PHP_EOL; // Lancer

職業を分けていくとそれぞれ固有の能力や技を持つようになるはずです。(そもそも固有のものをもたないなら想定クラスを分けないですよね)

Archarは特別な攻撃arrowを使うことで、Lancerは特殊な能力であるspeedUpを行使できるようにしました。ここでは分かりやすくするため文字を返しているだけですが、実際には値を更新したりするものと考えてください。


interface iSpeedup {
  public function speedUp();
}

interface iArrow {
  public function arrow();
}

class Archar extends Human implements iArrow
{
  public function getJob() { return 'Archar'; }
  public function getWeapon() { return 'bow'; }
  public function arrow() { return 'arrow'; }
}

class Lancer extends Human implements iSpeedup
{
  public function getJob() { return 'Lancer'; }
  public function getWeapon() { return 'lance'; }
  public function speedUp() { return 'speedUp'; }
}

$archar = new Archar('sea');
echo $archar->arrow() . PHP_EOL;

$lancer = new Lancer('mount');
echo $lancer->speedUp() . PHP_EOL;

ここで考えてみるべきはarrowやspeedUpは本当にArchar, Lancerの固有のものとして実装してしまって良いのかということです。

きっと最初に設計した時は完璧なものだったとしても後々のことを考えると完璧でないかもしれません。

interfaceで切り出しているので、正しいといえば正しいのですが、interfaceは実装の詳細を記述できません。

arrowの処理が別クラスでも必ず同じしたいといったような要件の達成は難しいでしょう。

例えば追加仕様としてspeedUpが使える「Thief」の実装が決まったとしたらどうしましょう。するとさすがにThiefクラスを作る際にLancerクラスを継承するのは許せないでしょう。

さらにこの「Thief」は実はボウガンを持っていてarrowも使えるようにしたいと言われたら現状の設計ではかなりの無理ゲーとなってしまいます。

phpでは多重継承はできませんので必然的にArchar, Lancerを両方継承することはできません。(できたとしてもダイヤモンド継承になるので嫌な感じです)

interfaceはあるので普通に両方実装すれば良いのですが、もしspeedUpやarrowが全職種を通して統一するべきものだとしたら、全く同じ内容を実装する各クラスにコピペすることになります。


class Thief extends Human implements iSpeedup,iArrow
{
  public function getJob() { return 'Thief'; }
  public function getWeapon() { return 'crossbow'; }
  public function arrow() { return 'arrow'; } // コピペ
  public function speedUp() { return 'speedUp'; } // コピペ
}

$thief = new Thief('river');
echo $thief->arrow() . PHP_EOL;
echo $thief->speedUp() . PHP_EOL;

このような時にtraitが役に立ちます。interfaceの実装をtraitで行ってしまうのです。

interfaceをクラスに組み込む場合、そのinterfaceのメソッドを全て実装する必要がありますが、これはそのクラス内でやる代わりにtraitでやってしまって問題ありません。

下記はspeedUpとarrowをtraitでやってみたものです。問題なく動作します。


trait SpeedUpTrait
{
  public function speedUp() { return 'speedUp'; }
}

trait ArrowTrait
{
  public function arrow() { return 'arrow'; }
}

class Thief extends Human implements iSpeedup,iArrow
{
  use SpeedUpTrait, ArrowTrait;
  public function getJob() { return 'Thief'; }
  public function getWeapon() { return 'crossbow'; }
}
$thief = new Thief('river');
echo $thief->arrow() . PHP_EOL;
echo $thief->speedUp() . PHP_EOL;

こうすることでspeedUpやarrowの実装箇所は1箇所に定まりました。使う職業が増えるごとにコピペをする必要がなくなります。

もちろんArcharやLancerでも同様にtraitを使うように修正します。


class Archar extends Human implements iArrow
{
  use ArrowTrait;
  public function getJob() { return 'Archar'; }
  public function getWeapon() { return 'bow'; }
}

class Lancer extends Human implements iSpeedup
{
  use SpeedUpTrait;
  public function getJob() { return 'Lancer'; }
  public function getWeapon() { return 'lance'; }
}

$archar = new Archar('sea');
echo $archar->arrow() . PHP_EOL; // arrow

$lancer = new Lancer('mount');
echo $lancer->speedUp() . PHP_EOL; // speedup

まとめ

いかがでしたでしょうか。今回はphpのとても便利な機能であるtraitを紹介しました。通常水平方向での機能の構成をする場合interfaceを使いますが、interfaceでは実装の詳細を持つことができませんでした。

しかしtraitを利用することで継承関係とは別軸で機能の再利用を達成することが可能となりました。traitを用いて再利用が高くバグの出にくいプログラムを書いていきましょう。