search
LoginSignup
91
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

PHPのトレイトが怖くて使えない

PHPのトレイトが怖くて使えない

by khsk
1 / 25

まとめ

トレイトの衝突が怖い

  • トレイトの安易な多用は衝突との戦いになりそう
  • トレイトは魅力的
  • トレイトでトレイトを使うってどうすれば上手くいくんだ?
  • 慎重に使っていても衝突は突然に。になりそう

対象(筆者知識)

class,static,extend,abstract,interface,trait
あたりを「機能としての使い方」はなんとなくわかっているが、
「正しい使い方」はよくわかってない人。


ふれないこと

  • ベストプラクティス
  • オブジェクト指向とはなんぞや
  • utility Class 不要論
  • グローバル関数

検証環境

  • PHP5.4

トレイトとは

PHP: トレイト - Manual

トレイトは、PHP のような単一継承言語でコードを再利用するための仕組みのひとつです。 トレイトは、単一継承の制約を減らすために作られたもので、 いくつかのメソッド群を異なるクラス階層にある独立したクラスで再利用できるようにします。 トレイトとクラスを組み合わせた構文は複雑さを軽減させてくれ、 多重継承や Mixin に関連するありがちな問題を回避することもできます。

トレイトはクラスと似ていますが、トレイトは単にいくつかの機能をまとめるためだけのものです。 トレイト自身のインスタンスを作成することはできません。 昔ながらの継承に機能を加えて、振る舞いを水平方向で構成できるようになります。 つまり、継承しなくてもクラスのメンバーに追加できるようになります。


私の解釈

public static functionの塊なUtilityClassの代わりとして使えそう。
UtilityClassよりもっと小さな機能毎に分割し、組み合わせ、
必要な機能だけをuse トレイトすることで、クラスにインポートというか実装できる。
また、クラスのメソッドとして実装されるので、$thisparentabstractが使えるのがメリットだ。

複数のクラスで使う便利な機能はトレイトで実装しよう
(おそらくキーワードとして「ふるまい」と「機能」は違う気がするのでここでズレ?)


重複宣言による衝突

トレイトは使用するクラスのプロパティ、メソッドとして追加するという性質から、
クラスの宣言や複数トレイト使用時に同名のものがあった場合、多くの場合で競合する。
詳しくはやっぱり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
print((newB)->a)すると

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による回避

Bのメソッドを使う
<?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といえば、BcollisionMethodを指すようになった。

問題点としては、
UserクラスでcollisionMethodと言えばBが持っているものを指す」
というわけではないので、衝突が3つになるとそれぞれ宣言が必要。
ただし、単一トレイトを使っている限り、なかなか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
CもBで代用すると宣言
<?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を使ってみる

`as`
<?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;
        A::collisionMethod as aliasMethod;
    }
}

$user = new User;
$user->sayA();
$user->sayB();
$user->aliasMethod(); // 違う、そうじゃない
出力
Bです
Bです
Aです

A::collisionMethod insteadof A::aliasMethod;とでも書ければよかったのだろうか?
とにかく、調べた限りではcollisionMethodは択一のようだ。


トレイトを使ったトレイトの衝突

うっかり同名のメソッドを宣言し、うっかりそれらのトレイトを同時に使ってしまう。
というシーンは、想像の範囲内ならばそう多くはないように思える。
適切な命名さえしておけば、ユニークな機能にユニークな名前が割り振られるだろう。

ところで、トレイトはトレイト内でトレイトを使うことができる。

クラスからトレイトを使えるのと同様に、トレイトからもトレイトを使えます。 トレイトの定義の中でトレイトを使うと、 定義したトレイトのメンバーの全体あるいは一部を組み合わせることができます。

「トレイト = 再利用したいクラス横断的便利機能」
という私の考えでは、次のような文章になる。


便利機能を使い、新しい便利機能を作る1


…問題になるのは、「再利用できるようにトレイトに書いたコードをトレイトで再利用したら衝突しちまった!」ということである…。

トレイト内でトレイト使わなければいいという縛りは使える時点で置いておいて。
使うメソッドと同じトレイトに書けばいいじゃないかというのもトレイトがトレイトを使えるというのに反している気がする。

とにかく、次のようなコードで衝突が起きる。


トレイのトレイトのメソッドの衝突
<?php

error_reporting(-1);
ini_set('log_errors','Off');
ini_set('display_errors', 'On');

trait collision {

    public function collisionMethod()
    {
        echo "collision\n";
    }

    public function unusedMethod()
    {
        echo "unused\n";
    }
}

trait A {
    use collision;

    public function sayA()
    {
        $this->collisionMethod();
        echo "Aです\n";
    }
}

trait B {
    use collision;

    public function sayB()
    {
        $this->collisionMethod();
        echo "Bです\n";
    }
}

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

UserクラスはただただトレイトA,Bの機能を使いたかっただけなのにトレイトcollisionについて怒られるのである。

トレイトA,Bだって、
「既存の機能を使って新しい機能を作りたかった」
だけである。

トレイトがどのトレイトと使われるかをトレイトが考慮するのは変だと思う。
かと言ってクラスが、全目的トレイトの全useトレイトの面倒を見て、前述の問題が生じないようにinsteadofを打っていくのも酷だと思う。

回避策

insteadofasでの回避策では数が増えた時にどうにも上手く行かない、行く気がしなかったので、抽象化を使うことにした。
トレイトではabstractが使えるので、依存する機能をabstractで宣言し、その実装に必要なトレイトの収集はクラスの義務としてみた。

抽象化を使った衝突回避
<?php

error_reporting(-1);
ini_set('log_errors','Off');
ini_set('display_errors', 'On');

trait collision {

    public function collisionMethod()
    {
        echo "collision\n";
    }

    public function unusedMethod()
    {
        echo "unused\n";
    }
}

trait A {
    abstract public function collisionMethod();

    public function sayA()
    {
        $this->collisionMethod();
        echo "Aです\n";
    }
}

trait B {
    abstract public function collisionMethod();

    public function sayB()
    {
        $this->collisionMethod();
        echo "Bです\n";
    }
}

class User {
    use collision;
    use A;
    use B;
}

$user = new User;
$user->sayA();
$user->sayB();
出力
collision
Aです
collision
Bです

不満点は、クラスで実装すべきabstractか、トレイトを集めるべきabstractか分からないことと、
トレイトでもabstract privateにはできないこと。
変更にも弱そう。
試していないが、AとBのcollisionMethodの実装は別物にしたい、というのも厳しいと思う。

そしてやはり「トレイトを使うトレイト」は諦めてしまっている。

締め

私は先の「トレイのトレイトのメソッドの衝突」が生じた時点で、
「小さなトレイトを作って、更にトレイト同士をレゴブロックの様に組み合わせてトレイトを作っていく」
という何と無しな構想が瓦解した。

でもUtilityクラスやグローバル関数も好きではないので、
横断的によく使う配列・文字列処理や、h()関数などはトレイトで作りたくなる。
トレイトを作っていると、やはり共通部分がでてきてトレイトに切り出したくなる->衝突が怖い…というループに陥っている。


PHP(単一継承言語)しか使ってこなかったので、Scalaのトレイトや多重継承やRubyのmix-inなどの考えが土台にあれば迷わない問題なのかもしれない。
また、オブジェクト指向を正しく使いこなせれば継承で綺麗に解決しているのかもしれない。


スライドモードってどうなんでしょうか。
テキストが見づらくなっちゃうのが難点かも。


  1. 見出しは目次優先なんだけどスライドでばーんと出したい 

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
What you can do with signing up
91
Help us understand the problem. What are the problem?