まとめ
トレイトの衝突が怖い
- トレイトの安易な多用は衝突との戦いになりそう
- トレイトは魅力的
- トレイトでトレイトを使うってどうすれば上手くいくんだ?
- 慎重に使っていても衝突は突然に。になりそう
対象(筆者知識)
class
,static
,extend
,abstract
,interface
,trait
あたりを「機能としての使い方」はなんとなくわかっているが、
「正しい使い方」はよくわかってない人。
ふれないこと
- ベストプラクティス
- オブジェクト指向とはなんぞや
- utility Class 不要論
- グローバル関数
検証環境
- PHP5.4
トレイトとは
トレイトは、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
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
を打っていくのも酷だと思う。
回避策
insteadof
やas
での回避策では数が増えた時にどうにも上手く行かない、行く気がしなかったので、抽象化を使うことにした。
トレイトでは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
などの考えが土台にあれば迷わない問題なのかもしれない。
また、オブジェクト指向を正しく使いこなせれば継承で綺麗に解決しているのかもしれない。
スライドモードってどうなんでしょうか。
テキストが見づらくなっちゃうのが難点かも。
-
見出しは目次優先なんだけどスライドでばーんと出したい ↩