LoginSignup
15
3

More than 1 year has passed since last update.

この記事は、OPENLOGIアドベントカレンダー2022 10日目の記事です。

はじめに

rikutoです!
1年ほど前に、エンジニアにキャリアチェンジすると同時にオープンロジにジョインしました。

普段は主にPHPを使っているのですが、今回は僕が抱いたTraitに関する疑問を記事にしてみました。

Traitの存在意義がわからん

PHPにはTraitという機能があります。
よく見る説明でもありますが、簡単にいえばクラスの一部分を部品にしてコピペできる機能です。

めちゃ簡単な例を考えます。
ボタンを押すと数字が1増えていく、カウンターを模したクラスをつくってみます。

AddCountTrait
trait AddCountTrait
{
    public function addOne(): void
    {
        $this->count += 1;
    }
}
Counter
class Counter
{
    use AddCountTrait;

    public function __construct(int $initialCount)
    {
        $this->count = $initialCount;
    }

    public function getCount(): int
    {
        return $this->count;
    }
}
実行例
$counter = new Counter(0);
echo $counter->getCount() .PHP_EOL; 
// >>> 0
$counter->addOne();
echo $counter->getCount() .PHP_EOL;
// >>> 1

上記のコードと以下のコードは同じ結果になります。

Counter
class Counter
{
    public function __construct(int $initialCount)
    {
        $this->count = $initialCount;
    }

    public function getCount(): int
    {
        return $this->count;
    }

    public function addOne(): void
    {
        $this->count += 1;
    }
}

要は、AddCountTraitの中身が、use AddCountTrait;の一文によってCounterクラスにコピペされたように書くことができます。

ここで素朴な疑問が。

継承すればよくね?

例えば、カウンターが次の属性を必ず持つものであるとします。

  1. カウンターは現在のカウントを記録する整数(カウント値と呼ぶ)を持ち、任意の値で初期化する
  2. 使用者がカウント値を参照できる
  3. ボタンを押すと、カウント値が決まった量だけ増加する

これを前提とし、”ボタンを押すと1増えるカウンター”は以下のように書くことが出来ます。

BaseCounter
class BaseCounter
{
    public function __construct(int $initialCount)
    {
        $this->count = $initialCount;
    }

    public function getCount(): int
    {
        return $this->count;
    }
}
AddOneCounter
class AddOneCounter extends BaseCounter
{
    public function addOne(): void
    {
        $this->count += 1;
    }
}

1.と2.の属性を基底にし、3.を継承先のクラスで実装してみました。
実質、記事先頭のAddOneTraitをクラスで実装したもので、Traitでできることは継承でもOKなように思えます。

多重継承っぽく使う

そもそも、PHPは多重継承(1つのクラスに複数のクラスを継承させること)を許していません。
たとえば、バスケットボールの得点のように、1点〜3点が追加されるカウンタの実装を考えます。

カウンタ値に2を追加、3を追加する実装をそれぞれ別に切り出したい場合、

AddTwoCounter
// 2点を追加するカウンタ
class AddTwoCounter extends BaseCounter
{
    public function addTwo(): void
    {
        $this->count += 2;
    }
}
// 3点を追加するカウンタ
class AddThreeCounter
{
//....

を用意し、

BasketBallPointCounter
// 1〜3の得点を追加できるカウンタ
class BasketBallPointCounter extends AddOneCounter, AddTwoCounter, ... //NG

みたいなことはできない、ということです(他の言語ではできるものもあります)。

そもそもTraitは、多重継承できない言語で、それを実現するための手段として実装された という経緯がある機能です。
Traitを使うと、以下のように書くことが出来ます。

trait AddOneTrait
{
    public function addOne(): void
    {
        $this->count += 1;
    }
}

trait AddTwoTrait
{
    public function addTwo(): void
    {
        $this->count += 2;
    }
}

trait AddThreeTrait
{
// 省略
}
BasketBallPointCounter
// 1〜3の得点を追加できるカウンタ
class BasketBallPointCounter extends BaseCounter
{
    use AddOneTrait;
    use AddTwoTrait;
    use AddThreeTrait;
    // ...
}

Traitは一つのクラス内で複数使うことができるので、複数の実装をまとめて一つのクラスで利用するときに有効な手段の一つとなります。

interfaceで型を定義する

interfaceを使うとトレイトの型を定義できて、以下のような実装も出来ます。
シンプルに1ずつ増えるカウンターの実装です。

CounterInterface
interface CounterInterface
{
    public function getCount();
}
AddOneInterface
interface AddOneInterface
{
    public function addOne();
}
Counter
class Counter extends BaseCounter implements CounterInterface, AddOneInterface
// interfaceは複数使える
{
    use AddOneTrait;
    // ...
}

少し無理矢理な例になってしまったかもしれませんが、interfaceは一つのクラスに対して複数使うことができるので、interfaceでtraitの型を定義し、簡単に利用することができます。

最後に

Web+DB PRESS vol.130の解説では、次のような一文がありました。

“現状についても将来についても、トレイトは合成では簡単に実現しづらいような比較的マイナーなユースケースで使われるべきものです。”

「Web+DB PRESS vol.130 トレイトのユースケース」より

トレイトは、継承でも合成でも実現できない実装を行うときに使うもの、と考えておけばよさそうです。

言語によって思想が異なり、提供される機能も異なるのは面白いですね。

参考

15
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
3