PHP のトレイトに気をつける
普段 Scala でトレイトを使いまくってるけれども PHP にも 5.4 からトレイトが入った。
trait の良いところは多重継承のできない言語で多重継承っぽいことができることだ。 use, use とつけていけば、いくらでも追加できる。DRY に書けてよいことだ。
対して悪いところはいとも簡単に複雑で暗黙的な依存関係が生まれることだ。
例えばこんなの
<?php trait Greeting { public function say() { if ($this->location == 'ja') { echo 'こんにちは' . PHP_EOL; } else { echo 'Hello' . PHP_EOL; } } } class Location { } class US extends Location { use Greeting; private $location = 'us'; } class Japan extends Location { use Greeting; private $location = 'ja'; } $us = new US(); $us->say(); $ja = new Japan(); $ja->say();
US、Japan ともに Location クラスを継承している。 それぞれに挨拶をするメソッドを追加したいけれどもこれは継承で実装するのが適切だろうか? よくわからない。よし、トレイトを作ろう。という経緯で上のコードは書かれた、という設定。
このコードのどこが微妙か。それは Greeting トレイト が US, Japan クラスのフィールドに依存していることだ。しかも暗黙的に。
こういうコードは読みにくい。Greeting トレイト だけを読んでも $this->location
ってなに?という気持ちになるし、US、Japan クラスだけを読んでも、まさかその private フィールドに Greeting トレイトが依存しているとは思うまい。
これだけでもひどいが、もう少しやりすぎ感を出してみよう。
<?php trait Greeting { public function say() { if ($this->location == 'ja') { echo 'こんにちは' . PHP_EOL; } else { echo 'Hello' . PHP_EOL; } } } trait JapanLocation { private $location = 'ja'; } class Japan { use JapanLocation; use Greeting; } $ja = new Japan(); $ja->say();
Japan クラスは実装を持っていない。Greeting と JapanLocation を mix-in してやることでオブジェクトを合成している。Greeting は Japan に mix-in されているが、それだけでは動作しない、JapanLocation も一緒に mix-in してやることで初めて動作する。わかりづらい。Greeting トレイトを使うきは Location トレイトも mix-in してくださいね!ということはドキュメントにしっかり書こう。あと JapanLocation の $location
は PhpStorm が未使用の変数として警告してくるけど消しちゃだめだよ、これは一見意味のない変数に見えるけれど、実は Greeting メソッドと一緒に mix-in するときに使われるんだ、ってこともドキュメントにちゃんと書いておかなきゃね。
とまあ、そんなのはやりすぎに見えて説得力がないけれど、 trait を使いまくるとこういうところに行き着くだろう。 それに実を言うとこれは Scala ではよく見るパターンだ。
trait Location { val location: String } trait JapanLocation extends Location { val location: String = "ja" } trait Greeting { self: Location => def say(): Unit = { if (location == "ja") { println("こんにちは") } else { println("Hello") } } } object Japan extends JapanLocation with Greeting
このコードの何が PHP と違うのかと言うと、次の1行にある。
trait Greeting { self: Location =>
self: Location =>
という記述は self-type annotation
と呼ばれ、
この Greeting トレイトは Location 型でもありますよ。つまり Location 型のクラスやトレイトにのみ mix-in できますよ、という意味になる。使いかたを間違えるとコンパイルエラーになる。
このように Scala ではクラス間の関係を明示することができるし、しなければコンパイルが通らない。ドキュメントなどではなく、コードとして表現できる。Scala 最高だった。
Scala ではこのように小さな trait を組み合わせて大きなオブジェクトを合成するパターンは、ケーキ作りになぞらえて、Cake Pattern と呼ばれる。Cake Pattern を使うと実装の一部となっている trait を差し替えることができるのでコンパイル時 DI のような使われ方もしている。
とは言え、いくら型の制約があると言っても、やりすぎると当然コードは読みづらくなるし、コンパイルが遅くなるとか、パス依存型が絡んでややこしくなるなどといった問題はあるので、最近では「Cake Patternはアンチパターンだ」とする流れもある。
話がずれた。Scala の話じゃない。PHP ではどうすれば良いか。そんなのは簡単、というか今まで(<PHP5.3)どおりやれば良い。
<?php class Greeting { public static function say($location) { if ($location == 'ja') { echo 'こんにちは' . PHP_EOL; } else { echo 'Hello' . PHP_EOL; } } }
結局これだけでよかった。この実装はシンプルだ。 このファイルだけ見れば良いし、呼び出し元から見ても say は場所に依存するんだな、ということがわかる。 このクラス単体で動作し、テストも書きやすい。リファクタリングも PhpStorm で自動的にできる。
トレイトの使いどころ
フレームワーク側で便利な trait を提供したりするのはそこまで悪くはないと思う。ドキュメントなりサンプルコードなりがしっかりあって、ユーザーの前提知識となっていれば良い。(例えば Ruby の Enumerable みたいなの)
それから古いフレームワークのコードを見ていて、継承関係がやたら複雑で、トレイトがあったらもっとマシになるなあと思うことはあるので、多重継承がない言語で多重継承っぽいことをやりたい、というところでは使えば良い。そういうものなので。
それ以外の、今までスタティックメソッドやら委譲・集約とかで済んでたものをトレイトにする、というのはやらないほうがよい。
まとめ
トレイトは柔軟で便利ではあるけれども、特に PHP のように型の制約のない言語で使用すると、 複雑で暗黙的なオブジェクト間の関係を生み出しコードはひどく読みづらくなる。 密結合で壊れやすく、変更が難しいコードになる。
トレイトはどうしても多重継承したい、という場面で使う。しかしそもそもを言えば「継承より委譲・集約を選ぶ」べきである。
暗黙的な知識を要求するトレイトを使うときはきちんとドキュメントを書くなり知識の共有を行うなどする。
結局は trait が悪いというよりは書き方の問題であるけれども、trait が意図せずにクラス間の結合を生み出す危険性が高いことは確か。話がややこしくなるから Ruby の module には触れなかったけど、同じような辛い場面には遭遇する。trait をたくさん使いたかったら Scala を書けば良い。