この記事は、OPENLOGIアドベントカレンダー2022 10日目の記事です。
はじめに
rikutoです!
1年ほど前に、エンジニアにキャリアチェンジすると同時にオープンロジにジョインしました。
普段は主にPHPを使っているのですが、今回は僕が抱いたTraitに関する疑問を記事にしてみました。
Traitの存在意義がわからん
PHPにはTraitという機能があります。
よく見る説明でもありますが、簡単にいえばクラスの一部分を部品にしてコピペできる機能です。
めちゃ簡単な例を考えます。
ボタンを押すと数字が1増えていく、カウンターを模したクラスをつくってみます。
trait AddCountTrait
{
public function addOne(): void
{
$this->count += 1;
}
}
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
上記のコードと以下のコードは同じ結果になります。
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増えるカウンター”は以下のように書くことが出来ます。
class BaseCounter
{
public function __construct(int $initialCount)
{
$this->count = $initialCount;
}
public function getCount(): int
{
return $this->count;
}
}
class AddOneCounter extends BaseCounter
{
public function addOne(): void
{
$this->count += 1;
}
}
1.と2.の属性を基底にし、3.を継承先のクラスで実装してみました。
実質、記事先頭のAddOneTrait
をクラスで実装したもので、Traitでできることは継承でもOKなように思えます。
多重継承っぽく使う
そもそも、PHPは多重継承(1つのクラスに複数のクラスを継承させること)を許していません。
たとえば、バスケットボールの得点のように、1点〜3点が追加されるカウンタの実装を考えます。
カウンタ値に2を追加、3を追加する実装をそれぞれ別に切り出したい場合、
// 2点を追加するカウンタ
class AddTwoCounter extends BaseCounter
{
public function addTwo(): void
{
$this->count += 2;
}
}
// 3点を追加するカウンタ
class AddThreeCounter
{
//....
を用意し、
// 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
{
// 省略
}
// 1〜3の得点を追加できるカウンタ
class BasketBallPointCounter extends BaseCounter
{
use AddOneTrait;
use AddTwoTrait;
use AddThreeTrait;
// ...
}
Traitは一つのクラス内で複数使うことができるので、複数の実装をまとめて一つのクラスで利用するときに有効な手段の一つとなります。
interfaceで型を定義する
interface
を使うとトレイトの型を定義できて、以下のような実装も出来ます。
シンプルに1ずつ増えるカウンターの実装です。
interface CounterInterface
{
public function getCount();
}
interface AddOneInterface
{
public function addOne();
}
class Counter extends BaseCounter implements CounterInterface, AddOneInterface
// interfaceは複数使える
{
use AddOneTrait;
// ...
}
少し無理矢理な例になってしまったかもしれませんが、interfaceは一つのクラスに対して複数使うことができるので、interfaceでtraitの型を定義し、簡単に利用することができます。
最後に
Web+DB PRESS vol.130の解説では、次のような一文がありました。
“現状についても将来についても、トレイトは合成では簡単に実現しづらいような比較的マイナーなユースケースで使われるべきものです。”
「Web+DB PRESS vol.130 トレイトのユースケース」より
トレイトは、継承でも合成でも実現できない実装を行うときに使うもの、と考えておけばよさそうです。
言語によって思想が異なり、提供される機能も異なるのは面白いですね。
参考
- 独習PHP 第4版, 山田祥寛, 2021, 翔泳社
- WEB+DB PRESS Vol.130 “トレイトでのコードの再利用とどう向き合うか”, 2022, 技術評論社