web-dev-qa-db-ja.com

特性とインターフェース

私は最近PHPについて勉強しようとしていましたが、私は自分自身が特性にとらわれていることに気付きました。私は水平方向のコードの再利用の概念を理解していて、必ずしも抽象クラスから継承したくないと思っています。私が理解していないのは、特性を使用することとインターフェースを使用することとの間の重大な違いは何ですか?

どちらを使用するかを説明するまともなブログ記事または記事を検索してみましたが、これまでに見つかった例は非常によく似ているようです。

これに関する意見や見解を他の誰かが共有できますか?

309

インターフェースは、実装クラスが実装しなければならないメソッドのセットを定義します。

特性がusenameである場合、メソッドの実装も行われます - これはInterfacename__では発生しません。

それが最大の違いです。

PHP RFCの水平方向の再利用 より:

Traitsは、PHPなどの単一継承言語でコードを再利用するためのメカニズムです。 Traitは、開発者が異なるクラス階層に存在するいくつかの独立したクラスでメソッドのセットを自由に再利用できるようにすることで、単一継承の制限を減らすことを目的としています。

224
Alec Gorge

公共サービス発表:

私は、その特性はほとんどの場合コードの匂いであり、構成のために避けるべきであると信じていると記録に述べたいと思います。私の考えでは、単一継承はアンチパターンであるという点まで頻繁に悪用され、多重継承はこの問題を悪化させるだけです。ほとんどの場合、継承よりも構成を優先することで(単数でも複数でも)、はるかに優れたサービスが得られます。それでも特性とそれらのインターフェースとの関係に興味があるなら、読んでください...


これを言うことから始めましょう:

オブジェクト指向プログラミング(OOP)は把握するのが難しいパラダイムです。クラスを使用しているからといって、コードがオブジェクト指向(OO)であるとは限りません。

OOコードを書くためには、OOPは本当にあなたのオブジェクトの機能に関するものであることを理解する必要があります。あなたは彼らがすることの代わりにクラスについて考える必要がありますができることの代わりに実際にします。これは、ちょっとしたコードを「何かする」ことに焦点が当てられている従来の手続き型プログラミングとはまったく対照的です。

OOPコードが計画と設計に関するものである場合、インターフェースは設計図であり、オブジェクトは完全に構築された家です。その間、特性は単に青写真(インターフェース)によってレイアウトされた家を建てるのを助ける方法です。

インターフェース

それでは、なぜインタフェースを使うべきなのでしょうか。非常に簡単に言うと、インターフェースはコードを脆弱にしません。あなたがこの声明を疑うならば、インターフェースに対して書かれていなかったレガシーコードを維持することを余儀なくされた誰かに尋ねなさい。

インターフェースはプログラマーと彼/彼女のコードの間の契約です。インターフェースには、「自分のルールに従ってプレイすれば、自分でも実装できますが、他のコードを壊さないことを約束します」と書かれています。

例として、現実のシナリオを考えてみましょう(自動車やウィジェットはありません)。

Webアプリケーションのキャッシュシステムを実装してサーバーの負荷を軽減したい

APCを使ってリクエストのレスポンスをキャッシュするクラスを書くことから始めます。

class ApcCacher
{
  public function fetch($key) {
    return apc_fetch($key);
  }
  public function store($key, $data) {
    return apc_store($key, $data);
  }
  public function delete($key) {
    return apc_delete($key);
  }
}

次に、httpレスポンスオブジェクトで、実際のレスポンスを生成するためのすべての作業を行う前にキャッシュヒットを確認します。

class Controller
{
  protected $req;
  protected $resp;
  protected $cacher;

  public function __construct(Request $req, Response $resp, ApcCacher $cacher=NULL) {
    $this->req    = $req;
    $this->resp   = $resp;
    $this->cacher = $cacher;

    $this->buildResponse();
  }

  public function buildResponse() {
    if (NULL !== $this->cacher && $response = $this->cacher->fetch($this->req->uri()) {
      $this->resp = $response;
    } else {
      // build the response manually
    }
  }

  public function getResponse() {
    return $this->resp;
  }
}

この方法はうまくいきます。しかし、数週間後には、APCの代わりにファイルベースのキャッシュシステムを使用することにしました。 ApcCacherクラスの機能を表すインターフェースではなく、ApcCacherクラスの機能で動作するようにコントローラをプログラムしたため、コントローラコードを変更する必要があります。上記の代わりに、Controllerクラスを具象のCacherInterfaceではなくApcCacherに依存するようにしたとしましょう。

// your controller's constructor using the interface as a dependency
public function __construct(Request $req, Response $resp, CacherInterface $cacher=NULL)

それに沿って進むために、あなたはそのようにあなたのインターフェースを定義します:

interface CacherInterface
{
  public function fetch($key);
  public function store($key, $data);
  public function delete($key);
}

次に、ApcCacherクラスと新しいFileCacherクラスの両方にCacherInterfaceを実装し、インターフェイスに必要な機能を使用するようにControllerクラスをプログラムします。

この例は(うまくいけば)、インターフェースへのプログラミングによって、他のコードが変更されても心配することなくクラスの内部実装を変更できることを示しています。

特性

一方、トレイトは単にコードを再利用するための方法です。インタフェースは、特性に対する相互に排他的な代替手段として考えるべきではありません。実際、インタフェースに必要な機能を満たす特性を作成することは理想的なユースケースです

複数のクラスが同じ機能を共有している場合にのみ特性を使用する必要があります(おそらく同じインターフェイスによって指示されます)。 1つのクラスに機能を提供するために特性を使用することに意味はありません。それはクラスが行うことを難読化し、より良い設計は特性の機能を関連するクラスに移動させるでしょう。

以下のトレイト実装を考えてください。

interface Person
{
    public function greet();
    public function eat($food);
}

trait EatingTrait
{
    public function eat($food)
    {
        $this->putInMouth($food);
    }

    private function putInMouth($food)
    {
        // digest delicious food
    }
}

class NicePerson implements Person
{
    use EatingTrait;

    public function greet()
    {
        echo 'Good day, good sir!';
    }
}

class MeanPerson implements Person
{
    use EatingTrait;

    public function greet()
    {
        echo 'Your mother was a hamster!';
    }
}

より具体的な例:インターフェースの説明からあなたのFileCacherとあなたのApcCacherの両方が同じ方法でキャッシュエントリが古くなっていて削除すべきかどうかを判断すると想像してみてください。トレイトを書いて、両方のクラスがそれを共通のインターフェース要件のために使うことを可能にすることができます。

最後に注意することが1つあります。特性を追い越さないように注意してください。ユニークなクラスの実装で十分な場合は、特性が貧弱なデザインの松葉杖としてよく使用されます。最良のコード設計のためには、特性をインターフェース要件を満たすことに限定する必要があります。

499
rdlowrey

traitは基本的にPHPによるmixinの実装であり、事実上traitを追加することであらゆるクラスに追加できる一連の拡張メソッドです。メソッドはそのクラスの実装の一部になりますが、継承を使用しないです。

から PHPマニュアル (私の強調):

特性は、PHPのような単一継承言語におけるコードの再利用のためのメカニズムです。 ...これは伝統的な継承への追加であり、振る舞いの水平的合成を可能にします。つまり、継承を必要としないクラスメンバーの適用です。

例:

trait myTrait {
    function foo() { return "Foo!"; }
    function bar() { return "Bar!"; }
}

上記の特性を定義したら、次のことができます。

class MyClass extends SomeBaseClass {
    use myTrait; // Inclusion of the trait myTrait
}

この時点で、クラスMyClassのインスタンスを作成すると、myTraitから派生したfoo()bar()という2つのメソッドがあります。そして - trait-定義されたメソッドはすでにメソッド本体を持っていることに注意してください - Interface-definedメソッドはできません。

さらに - 他の多くの言語と同様に、PHPは単一継承モデルを使用します - つまり、クラスは複数のインターフェースから派生することができますが、複数のクラスから派生することはできません。ただし、PHP class canには複数のtraitを含めることができます。これにより、プログラマは再利用可能な要素を含めることができます。

注意すべき点がいくつかあります。

                      -----------------------------------------------
                      |   Interface   |  Base Class   |    Trait    |
                      ===============================================
> 1 per class         |      Yes      |       No      |     Yes     |
---------------------------------------------------------------------
Define Method Body    |      No       |       Yes     |     Yes     |
---------------------------------------------------------------------
Polymorphism          |      Yes      |       Yes     |     No      |
---------------------------------------------------------------------

多態性:

前の例では、MyClassextendsSomeBaseClassの場合、MyClassSomeBaseClassのインスタンスです。つまり、SomeBaseClass[] basesなどの配列にMyClassのインスタンスを含めることができます。同様に、MyClassIBaseInterfaceを拡張した場合、IBaseInterface[] basesの配列にMyClassのインスタンスを含めることができます。 traitにはそのような多態的な構文はありません - traitは基本的に単なるコードであり、プログラマの便宜のためにそれを使用する各クラスにコピーされます。

優先順位:

マニュアルに記載されているように:

基本クラスから継承されたメンバは、Traitによって挿入されたメンバによって上書きされます。優先順位は、現在のクラスのメンバがTraitメソッドをオーバーライドすることです。Traitメソッドは、継承メソッドをオーバーライドします。

そのため、次のシナリオを検討してください。

class BaseClass {
    function SomeMethod() { /* Do stuff here */ }
}

interface IBase {
    function SomeMethod();
}

trait myTrait {
    function SomeMethod() { /* Do different stuff here */ }
}

class MyClass extends BaseClass implements IBase {
    use myTrait;

    function SomeMethod() { /* Do a third thing */ }
}

上記のMyClassのインスタンスを作成すると、次のことが起こります。

  1. InterfaceIBaseは、提供されるSomeMethod()と呼ばれるパラメータのない関数を必要とします。
  2. 基本クラスBaseClassは、このメソッドの実装を提供します - ニーズを満たします。
  3. traitmyTraitSomeMethod()と呼ばれるパラメータなしの関数も提供します。BaseClass- versionよりも優先されます
  4. classMyClassは、独自のバージョンのSomeMethod() - を提供し、これはtrait-バージョンよりも優先されます

まとめ

  1. Interfaceはメソッド本体のデフォルト実装を提供できませんが、traitは提供できません。
  2. Interfacepolymorphicinherited構文です - traitはそうではありません。
  3. 同じクラス内で複数のInterfacesを使用でき、複数のtraitsも使用できます。
62
Troy Alford

traitsは、いくつかの異なるクラスのメソッドとして使用できるメソッドを含むクラスを作成するのに役立ちます。

例えば:

trait ToolKit
{
    public $errors = array();

    public function error($msg)
    {
        $this->errors[] = $msg;
        return false;
    }
}

あなたはがこの特性を使うどんなクラスでもこの "error"メソッドを持って使うことができます。

class Something
{
    use Toolkit;

    public function do_something($zipcode)
    {
        if (preg_match('/^[0-9]{5}$/', $zipcode) !== 1)
            return $this->error('Invalid zipcode.');

        // do something here
    }
}

interfacesでは、メソッドのシグネチャのみ宣言できますが、その関数のコードは宣言できません。また、インターフェースを使用するには、implementsを使用して階層をたどる必要があります。これは形質には当てはまりません。

全然違います!

25
J. Bruni

上記の初心者のために答えは難しいかもしれません、これはそれを理解する最も簡単な方法です:

特性

trait SayWorld {
    public function sayHello() {
        echo 'World!';
    }
}

そのため、関数全体を作り直さずに他のクラスでsayHello関数を使いたい場合は、トレイトを使うことができます。

class MyClass{
  use SayWorld;

}

$o = new MyClass();
$o->sayHello();

いいね!

関数だけでなく、あなたはその特色の中で何でも使うことができます(function、variables、const ..)。複数のトレイトを使うこともできます:use SayWorld,AnotherTraits;

インターフェース

  interface SayWorld {
     public function sayHello();
  }

  class MyClass implements SayWorld { 
     public function sayHello() {
        echo 'World!';
     }
}

ですから、これはインターフェースが特性とどう違うのかということです:あなたは実装されたクラスのインターフェースですべてを作り直さなければなりません。インターフェースは実装されていません。また、interfaceは関数とconstしか持つことができず、変数を持つことはできません。

これが役に立つことを願っています!

18
Supun Praneeth

特性を記述するためによく使われる比喩は、特性は実装とのインターフェースです。

これはほとんどの状況でそれについて考える良い方法ですが、両者の間には微妙な違いがいくつかあります。

最初は、instanceof演算子は特性を処理しません(つまり、特性は実際のオブジェクトではありません)。そのため、クラスに特定の特性があるかどうかを確認することはできません。特性)。それが、水平コードの再利用のための構成要素であるという意味です。

そこにare関数がPHPに含まれています。これはクラスが使うすべての特性のリストを得ることを可能にしますが、特性継承は確実にチェックするために再帰的チェックをする必要があることを意味しますある時点でクラスが特定の特徴を持っている場合(PHP docoページにサンプルコードがあります)。しかし、ええ、それはinstanceofのように単純できれいではないことは確かです。そして私見はPHPをより良くするための機能です。

また、抽象クラスは依然としてクラスなので、多重継承に関連するコードの再利用に関する問題は解決されません。 1つのクラス(実または抽象)のみを拡張でき、複数のインターフェースを実装できることを忘れないでください。

私は、特性とインターフェースが擬似多重継承を作成するために手をつないで使用するのが本当に良いことを発見しました。例えば:

class SlidingDoor extends Door implements IKeyed  
{  
    use KeyedTrait;  
    [...] // Generally not a lot else goes here since it's all in the trait  
}

これを行うことで、instanceofを使用して特定のDoorオブジェクトがKeyedかどうかを判断できます。一貫した一連のメソッドなどが得られ、KeyedTraitを使用するすべてのクラスですべてのコードが1か所に配置されます。

4
Jon Kloske

Traitsは単にcode reuseのためのものです。

Interfaceは提供されるべき関数のsignatureを提供します[クラス内で定義どこで使用できるかプログラマの]裁量。したがって、クラスのグループに対してprototypeを与えます。

参考のために--- http://www.php.net/manual/en/language.oop5.traits.php

4
Rajesh Paul

基本的に、Traitはコードの自動「コピー&ペースト」と見なすことができます。

実行する前にそれが何をするのかを知ることは意味がないのでTraitsを使うことは危険です。

ただし、特性は継承などの制限がないため、より柔軟です。

トレイトは何かをクラスにチェックインするメソッドをインジェクトするのに役立ちます。別のメソッドまたは属性の存在 それについてのいい記事(フランス語で、すみません)

それを手に入れることができるフランス語を読む人々のために、GNU/Linux Magazine HS 54はこの主題に関する記事を持っています。

3
Benj

あなたが英語を知っていてtraitが何を意味するのか知っていれば、それはまさにその名前が言うことです。これはuseと入力して既存のクラスにアタッチするメソッドとプロパティのクラスレスパックです。

基本的には、それを単一の変数と比較できます。クロージャ関数はスコープの外側からこれらの変数をuseすることができ、そのようにしてそれらは内側に値を持ちます。それらは強力であり、すべてに使用できます。それらが使われているならば、同じことが特性にも起こります。

2
Thielicious

他の答えはインターフェースと特性の違いを説明するのに素晴らしい仕事をしました。私は実用的な便利な例に焦点を当てます。特に、トレイトがインスタンス変数を使用できることを実証する例を示します - 最小限の定型コードでクラスに振る舞いを追加できるようにします。

繰り返しますが、他の人が述べたように、トレイトはインタフェースとうまく組み合わさり、インタフェースが動作コントラクトを指定し、トレイトが実装を満たすことを可能にします。

イベント発行/購読機能をクラスに追加することは、一部のコードベースでは一般的なシナリオになります。 3つの一般的な解決策があります。

  1. イベントパブ/サブコードで基本クラスを定義し、そしてイベントを提供したいクラスは機能を得るためにそれを拡張することができます。
  2. イベントpub/subコードでクラスを定義し、そしてイベントを提供したい他のクラスは合成を通してそれを使うことができます。そして、それに対するメソッド呼び出しをプロキシして、合成されたオブジェクトをラップする独自のメソッドを定義します。
  3. イベントpub/subコードでトレイトを定義し、そしてイベントを提供したい他のクラスはトレイトをuseトレイト(別名それをインポートする)して能力を得ることができます。

それぞれはどの程度うまくいきますか?

#1うまくいかないそれは、あなたがすでに何か他のものを拡張しているので、あなたが基本クラスを拡張することができないと気づく日までです。このような継承を使用することがどの程度制限されるかは明らかであるはずなので、この例は示しません。

#2と#3はどちらもうまく機能します。いくつかの違いを強調した例を示します。

まず、両方の例で同じコードがいくつかあります。

インターフェース

interface Observable {
    function addEventListener($eventName, callable $listener);
    function removeEventListener($eventName, callable $listener);
    function removeAllEventListeners($eventName);
}

使い方を示すためのコードもあります。

$auction = new Auction();

// Add a listener, so we know when we get a bid.
$auction->addEventListener('bid', function($bidderName, $bidAmount){
    echo "Got a bid of $bidAmount from $bidderName\n";
});

// Mock some bids.
foreach (['Moe', 'Curly', 'Larry'] as $name) {
    $auction->addBid($name, Rand());
}

それでは、トレイトを使用した場合のAuctionクラスの実装がどのように異なるのかを説明しましょう。

まず、#2(コンポジションを使用)がどのようになるかを次に示します。

class EventEmitter {
    private $eventListenersByName = [];

    function addEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName][] = $listener;
    }

    function removeEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName] = array_filter($this->eventListenersByName[$eventName], function($existingListener) use ($listener) {
            return $existingListener === $listener;
        });
    }

    function removeAllEventListeners($eventName) {
        $this->eventListenersByName[$eventName] = [];
    }

    function triggerEvent($eventName, array $eventArgs) {
        foreach ($this->eventListenersByName[$eventName] as $listener) {
            call_user_func_array($listener, $eventArgs);
        }
    }
}

class Auction implements Observable {
    private $eventEmitter;

    public function __construct() {
        $this->eventEmitter = new EventEmitter();
    }

    function addBid($bidderName, $bidAmount) {
        $this->eventEmitter->triggerEvent('bid', [$bidderName, $bidAmount]);
    }

    function addEventListener($eventName, callable $listener) {
        $this->eventEmitter->addEventListener($eventName, $listener);
    }

    function removeEventListener($eventName, callable $listener) {
        $this->eventEmitter->removeEventListener($eventName, $listener);
    }

    function removeAllEventListeners($eventName) {
        $this->eventEmitter->removeAllEventListeners($eventName);
    }
}

#3(特性)は次のようになります。

trait EventEmitterTrait {
    private $eventListenersByName = [];

    function addEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName][] = $listener;
    }

    function removeEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName] = array_filter($this->eventListenersByName[$eventName], function($existingListener) use ($listener) {
            return $existingListener === $listener;
        });
    }

    function removeAllEventListeners($eventName) {
        $this->eventListenersByName[$eventName] = [];
    }

    protected function triggerEvent($eventName, array $eventArgs) {
        foreach ($this->eventListenersByName[$eventName] as $listener) {
            call_user_func_array($listener, $eventArgs);
        }
    }
}

class Auction implements Observable {
    use EventEmitterTrait;

    function addBid($bidderName, $bidAmount) {
        $this->triggerEvent('bid', [$bidderName, $bidAmount]);
    }
}

EventEmitterTrait内のコードは、トレイトがtriggerEvent()メソッドをprotectedとして宣言している点を除けば、EventEmitterクラス内のコードとまったく同じです。つまり、唯一注意する必要があるのはAuctionクラスの実装です

そして違いは大きいです。コンポジションを使うとき、私たちはEventEmitterを好きなだけ多くのクラスで再利用することを可能にする、すばらしい解決策を得ます。しかし、主な欠点は、Observableインターフェースで定義された各メソッドに対して、対応するメソッドに引数を転送するだけの退屈な定型コードを記述する必要があるため、作成および保守が必要な定型コードが多数あることです。私たちの合成ではEventEmitterオブジェクトです。 この例の特性はそれを避けることを可能にしますを助けてくれます定型コードを減らして保守性を向上させます

ただし、Auctionクラスで完全なObservableインターフェースを実装したくない場合があります。1つか2つのメソッドを公開したい場合、または独自のメソッドシグネチャを定義できるようにまったくしたくない場合があります。そのような場合、あなたはまだ合成方法を好むかもしれません。

しかし、ほとんどのシナリオでトレイトは非常に説得力があります。特に、インターフェースにたくさんのメソッドがある場合は、たくさんの定型句を書く必要があります。

*実際にはその両方を行うこともできます - EventEmitterクラスを構成的に使用したい場合はEventEmitterTraitクラスを定義し、EventEmitterクラスの実装もtrait内で定義します:)

2
goat

インターフェースは「このオブジェクトはこのことを行うことができる」と言う契約ですが、特性はオブジェクトにそのことをする能力を与えています。

Traitは本質的にクラス間でコードを「コピー&ペースト」する方法です。

この記事を読んでみてください

1
Hos Mercury

その特性は、多重継承の目的やコードの再利用性のために使用できるクラスと同じです。

クラス内で特性を使用することができ、また 'use keyword'を使用して同じクラス内で複数の特性を使用することができます。

インターフェースはトレイトと同じコードの再利用性のために使用しています

インターフェースは複数のインターフェースを拡張するので、多重継承の問題を解決できますが、インターフェースを実装するときには、クラス内にすべてのメソッドを作成する必要があります。詳細については、リンクの下をクリックしてください。

http://php.net/manual/en/language.oop5.traits.phphttp://php.net/manual/en/language.oop5.interfaces.php

1

主な違いは、インタフェースでは、そのインタフェースを実装する各クラス内で各メソッドの実際の実装を定義する必要があるということです。クラスもう1つの重要な違いは、インスタンスメソッドであることができる(そして通常そうである)インタフェースメソッドとは異なり、traitメソッドはclassメソッドまたはstaticメソッドにしかなれないことです。

0