まとめ
トレイトの衝突が怖い
- トレイトの安易な多用は衝突との戦いになりそう
- トレイトは魅力的
- トレイトでトレイトを使うってどうすれば上手くいくんだ?
- 慎重に使っていても衝突は突然に。になりそう
対象(筆者知識)
class,static,extend,abstract,interface,trait
あたりを「機能としての使い方」はなんとなくわかっているが、
「正しい使い方」はよくわかってない人。
ふれないこと
- ベストプラクティス
- オブジェクト指向とはなんぞや
- utility Class 不要論
- グローバル関数
検証環境
- PHP5.4
トレイトとは
[PHP: トレイト - Manual]
トレイトは、PHP のような単一継承言語でコードを再利用するための仕組みのひとつです。 トレイトは、単一継承の制約を減らすために作られたもので、 いくつかのメソッド群を異なるクラス階層にある独立したクラスで再利用できるようにします。 トレイトとクラスを組み合わせた構文は複雑さを軽減させてくれ、 多重継承や Mixin に関連するありがちな問題を回避することもできます。
トレイトはクラスと似ていますが、トレイトは単にいくつかの機能をまとめるためだけのものです。 トレイト自身のインスタンスを作成することはできません。 昔ながらの継承に機能を加えて、振る舞いを水平方向で構成できるようになります。 つまり、継承しなくてもクラスのメンバーに追加できるようになります。
私の解釈
public static functionの塊なUtilityClassの代わりとして使えそう。
UtilityClassよりもっと小さな機能毎に分割し、組み合わせ、
必要な機能だけをuse トレイトすることで、クラスにインポートというか実装できる。
また、クラスのメソッドとして実装されるので、$thisやparent、abstractが使えるのがメリットだ。
複数のクラスで使う便利な機能はトレイトで実装しよう
(おそらくキーワードとして「ふるまい」と「機能」は違う気がするのでここでズレ?)
重複宣言による衝突
トレイトは使用するクラスのプロパティ、メソッドとして追加するという性質から、
クラスの宣言や複数トレイト使用時に同名のものがあった場合、多くの場合で競合する。
詳しくはやっぱり[PHP: トレイト - Manual]参照。
プロパティ
問答無用で衝突する。
リネーム不可。
回避策としては頭にクラス名などをつけてユニークにする。
または、トレイトではプロパティは作らず、getter風メソッドの中で値を作成し返したりすることが考えられる。
<?php
error_reporting(-1);
ini_set('log_errors','Off');
ini_set('display_errors', 'On');
trait A {
public $a;
}
class B {
use A;
public $a;
}
new B;
Strict Standards: B and A define the same property ($a) in the composition of B. This might be incompatible, to improve maintainability consider using accessor methods in traits instead. Class was composed
Fatal error: B and A define the same property ($a) in the composition of B. However, the definition differs and is considered incompatible. Class was composed
詳しくは
トレイトでプロパティを定義したときは、クラスでは同じ名前のプロパティを定義できません。 定義しようとすると、エラーが発生します。クラス側での定義がトレイトでの定義と互換性がある (可視性も初期値も同じ) 場合は E_STRICT、 それ以外の場合は fatal error となります。
メソッド
メソッドの場合はプロパティより柔軟あるいは複雑になる。
トレイト・クラス間では優先順位が生じて、トレイトでparentが使えるようになったりと、便利機能のような扱い。あまり問題にはならない気がする。
トレイト・トレイト間の場合は、insteadofを使い、一方のトレイトのメソッドは他方のメソッドとして扱うことで回避できる。
また、プロパティと同じくトレイト名を頭につけるなどでユニークにする方法もあるでしょう。受け入れ難いですが…
シンプルな衝突
<?php
error_reporting(-1);
ini_set('log_errors','Off');
ini_set('display_errors', 'On');
trait A {
public function collisionMethod()
{
echo "Aです\n";
}
}
trait B {
public function collisionMethod()
{
echo "Bです\n";
}
}
class User {
use A;
use B;
}
(new User)->collisionMethod();
Fatal error: Trait method collisionMethod has not been applied, because there are collisions with other trait methods on User in
insteadofによる回避
<?php
error_reporting(-1);
ini_set('log_errors','Off');
ini_set('display_errors', 'On');
trait A {
public function collisionMethod()
{
echo "Aです\n";
}
}
trait B {
public function collisionMethod()
{
echo "Bです\n";
}
}
class User {
use A;
use B {
B::collisionMethod insteadof A;
}
}
(new User)->collisionMethod();
Bです
ちょっと混乱しますが、Aの代わりにBを使う、です。
collisionMethodといえば、BのcollisionMethodを指すようになった。
問題点としては、
「UserクラスでcollisionMethodと言えばBが持っているものを指す」
というわけではないので、衝突が3つになるとそれぞれ宣言が必要。
ただし、単一トレイトを使っている限り、なかなか3つ以上衝突することは稀な気がする。
<?php
error_reporting(-1);
ini_set('log_errors','Off');
ini_set('display_errors', 'On');
trait A {
public function collisionMethod()
{
echo "Aです\n";
}
}
trait B {
public function collisionMethod()
{
echo "Bです\n";
}
}
trait C {
public function collisionMethod()
{
echo "Cです\n";
}
}
class User {
use A;
use B {
B::collisionMethod insteadof A;
}
use C;
}
(new User)->collisionMethod();
Fatal error: Trait method collisionMethod has not been applied, because there are collisions with other trait methods on User
<?php
error_reporting(-1);
ini_set('log_errors','Off');
ini_set('display_errors', 'On');
trait A {
public function collisionMethod()
{
echo "Aです\n";
}
}
trait B {
public function collisionMethod()
{
echo "Bです\n";
}
}
trait C {
public function collisionMethod()
{
echo "Cです\n";
}
}
class User {
use A;
use B {
B::collisionMethod insteadof A, C;
}
use C;
}
(new User)->collisionMethod();
Bです
insteadofを使うと、他方のメソッドを参照できなくなりますが、
asを使い別名のエイリアスをつけることで保持し続けられます。
勘違いしていたこととして、asを使えばinsteadofは不要かと思いましたが、asはあくまでinsteadofで消えたメソッドの迂回策のようです。
衝突したメソッドを使ったトレイトのメソッド
続いて、もう少し複雑に。
先の例は衝突メソッドをクラスが呼んでいましたが、それぞれトレイト内で使われていたら…
<?php
error_reporting(-1);
ini_set('log_errors','Off');
ini_set('display_errors', 'On');
trait A {
public function collisionMethod()
{
echo "Aです\n";
}
public function sayA()
{
$this->collisionMethod();
}
}
trait B {
public function collisionMethod()
{
echo "Bです\n";
}
public function sayB()
{
$this->collisionMethod();
}
}
class User {
use A;
use B;
}
$user = new User;
$user->sayA();
$user->sayB();
Fatal error: Trait method collisionMethod has not been applied, because there are collisions with other trait methods on User
insteadofで回避します。
<?php
error_reporting(-1);
ini_set('log_errors','Off');
ini_set('display_errors', 'On');
trait A {
public function collisionMethod()
{
echo "Aです\n";
}
public function sayA()
{
$this->collisionMethod();
}
}
trait B {
public function collisionMethod()
{
echo "Bです\n";
}
public function sayB()
{
$this->collisionMethod();
}
}
class User {
use A;
use B {
B::collisionMethod insteadof A;
}
}
$user = new User;
$user->sayA();
$user->sayB();
Bです
Bです
collisionMethodがトレイトBの実装を使うようにしたため、トレイトAのsayAまで影響を受けてしまった。
asを使ってみる
```php:as