Eigenschaften vs. Schnittstellen


344

Ich habe in letzter Zeit versucht, mich mit PHP zu beschäftigen, und bin dabei, mich auf Eigenschaften einzulassen. Ich verstehe das Konzept der Wiederverwendung von horizontalem Code und möchte nicht unbedingt von einer abstrakten Klasse erben. Was ich nicht verstehe ist: Was ist der entscheidende Unterschied zwischen der Verwendung von Merkmalen und Schnittstellen?

Ich habe versucht, nach einem anständigen Blog-Beitrag oder Artikel zu suchen, in dem erklärt wird, wann der eine oder andere verwendet werden soll, aber die Beispiele, die ich bisher gefunden habe, scheinen so ähnlich zu sein, dass sie identisch sind.


6
Schnittstelle haben keinen Code in den Funktionskörpern. Sie haben tatsächlich keine Funktionskörper.
hakre

2
Trotz meiner vielbeachteten Antwort würde ich gerne festhalten, dass ich im Allgemeinen gegen Trait / Mixin bin . In diesem Chat-Protokoll erfahren Sie, wie häufig Merkmale solide OOP-Praktiken untergraben .
Rdlowrey

2
Ich würde das Gegenteil argumentieren. Nachdem ich jahrelang mit PHP gearbeitet habe und seit dem Aufkommen von Merkmalen, denke ich, ist es einfach, ihren Wert zu beweisen. Lesen Sie einfach dieses praktische Beispiel durch, das es 'Bildmodellen' ermöglicht, auch wie ImagickObjekte zu gehen und zu sprechen , abzüglich all des Aufblähens, das in den alten Tagen vor Merkmalen benötigt wurde.
Quickshiftin

Eigenschaften und Schnittstelle sind ähnlich. Der Hauptunterschied besteht darin, dass Sie mit den Traits die Methoden implementieren können, mit der Schnittstelle nicht.
John

Antworten:


238

Eine Schnittstelle definiert eine Reihe von Methoden, die die implementierende Klasse implementieren muss .

Wenn ein Merkmal vorhanden ist use, kommen auch die Implementierungen der Methoden hinzu - was in einem nicht der Fall ist Interface.

Das ist der größte Unterschied.

Aus der horizontalen Wiederverwendung für PHP RFC :

Traits ist ein Mechanismus zur Wiederverwendung von Code in einzelnen Vererbungssprachen wie PHP. Ein Merkmal soll einige Einschränkungen der Einzelvererbung verringern, indem es einem Entwickler ermöglicht wird, Methodensätze in mehreren unabhängigen Klassen, die in unterschiedlichen Klassenhierarchien leben, frei wiederzuverwenden.


2
@ JREAM In der Praxis nichts. In Wirklichkeit viel mehr.
Alec Gorge

79
Nur dass Merkmale überhaupt keine Schnittstellen sind. Schnittstellen sind Spezifikationen, anhand derer überprüft werden kann. Merkmale können nicht überprüft werden, daher sind sie nur eine Implementierung. Sie sind das genaue Gegenteil von Schnittstellen. Diese Zeile im RFC ist einfach falsch ...
ircmaxell

195
Merkmale sind im Wesentlichen sprachgestütztes Kopieren und Einfügen .
Shahid

10
Das ist keine Metapher. Das schlachtet die Bedeutung eines Wortes. Es ist, als würde man eine Box als eine Oberfläche mit Volumen beschreiben.
Cleong

6
So erweitern Sie die Kommentare von ircmaxell und Shadi: Sie können überprüfen, ob ein Objekt eine Schnittstelle implementiert (über instanceof), und Sie können sicherstellen, dass ein Methodenargument eine Schnittstelle über einen Typhinweis in der Methodensignatur implementiert. Sie können keine entsprechenden Überprüfungen auf Merkmale durchführen.
Brian D'Astous

530

Mitteilung des öffentlichen Dienstes:

Ich möchte festhalten, dass ich glaube, dass Merkmale fast immer ein Codegeruch sind und zugunsten der Komposition vermieden werden sollten. Ich bin der Meinung, dass eine einzelne Vererbung häufig bis zu einem Anti-Muster missbraucht wird und eine mehrfache Vererbung dieses Problem nur noch verstärkt. In den meisten Fällen werden Sie viel besser bedient, wenn Sie die Komposition der Vererbung vorziehen (sei es einzeln oder mehrfach). Wenn Sie immer noch an Merkmalen und deren Beziehung zu Schnittstellen interessiert sind, lesen Sie weiter ...


Beginnen wir damit:

Objektorientierte Programmierung (OOP) kann ein schwer zu fassendes Paradigma sein. Nur weil Sie Klassen verwenden, bedeutet dies nicht, dass Ihr Code objektorientiert (OO) ist.

Um OO-Code zu schreiben, müssen Sie verstehen, dass es bei OOP wirklich um die Funktionen Ihrer Objekte geht. Sie müssen über Klassen nachdenken, was sie können, anstatt was sie tatsächlich tun . Dies steht in krassem Gegensatz zur traditionellen prozeduralen Programmierung, bei der der Schwerpunkt darauf liegt, ein bisschen Code "etwas tun" zu lassen.

Wenn es beim OOP-Code um Planung und Design geht, ist eine Schnittstelle die Blaupause und ein Objekt das vollständig konstruierte Haus. In der Zwischenzeit sind Merkmale lediglich eine Möglichkeit, das durch die Blaupause (die Schnittstelle) festgelegte Haus zu bauen.

Schnittstellen

Warum sollten wir also Schnittstellen verwenden? Schnittstellen machen unseren Code ganz einfach weniger spröde. Wenn Sie an dieser Aussage zweifeln, fragen Sie jeden, der gezwungen war, Legacy-Code zu pflegen, der nicht für Schnittstellen geschrieben wurde.

Die Schnittstelle ist ein Vertrag zwischen dem Programmierer und seinem Code. Die Benutzeroberfläche sagt: "Solange Sie sich an meine Regeln halten, können Sie mich implementieren, wie Sie möchten, und ich verspreche, dass ich Ihren anderen Code nicht brechen werde."

Stellen Sie sich als Beispiel ein reales Szenario vor (keine Autos oder Widgets):

Sie möchten ein Caching-System für eine Webanwendung implementieren, um die Serverlast zu verringern

Sie schreiben zunächst eine Klasse, um Anforderungsantworten mit APC zwischenzuspeichern:

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);
  }
}

Anschließend suchen Sie in Ihrem HTTP-Antwortobjekt nach einem Cache-Treffer, bevor Sie die gesamte Arbeit ausführen, um die tatsächliche Antwort zu generieren:

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;
  }
}

Dieser Ansatz funktioniert hervorragend. Aber vielleicht ein paar Wochen später entscheiden Sie sich, ein dateibasiertes Cache-System anstelle von APC zu verwenden. Jetzt müssen Sie Ihren Controller-Code ändern, da Sie Ihren Controller so programmiert haben, dass er mit der Funktionalität der ApcCacherKlasse und nicht mit einer Schnittstelle arbeitet, die die Funktionen der ApcCacherKlasse ausdrückt . Nehmen wir an, Sie hätten die ControllerKlasse CacherInterfacestattdessen auf a anstatt auf Beton gesetzt ApcCacher:

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

Dazu definieren Sie Ihre Benutzeroberfläche folgendermaßen:

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

Im Gegenzug müssen sowohl Ihre ApcCacherals auch Ihre neuen FileCacherKlassen die implementieren CacherInterfaceund Ihre ControllerKlasse so programmieren , dass sie die für die Schnittstelle erforderlichen Funktionen nutzt.

Dieses Beispiel zeigt (hoffentlich), wie Sie durch Programmieren auf eine Schnittstelle die interne Implementierung Ihrer Klassen ändern können, ohne sich Sorgen machen zu müssen, ob die Änderungen Ihren anderen Code beschädigen.

Züge

Merkmale hingegen sind lediglich eine Methode zur Wiederverwendung von Code. Schnittstellen sollten nicht als sich gegenseitig ausschließende Alternative zu Merkmalen betrachtet werden. In der Tat ist das Erstellen von Merkmalen, die die für eine Schnittstelle erforderlichen Funktionen erfüllen, der ideale Anwendungsfall .

Sie sollten Merkmale nur verwenden, wenn mehrere Klassen dieselbe Funktionalität verwenden (wahrscheinlich von derselben Schnittstelle vorgegeben). Es macht keinen Sinn, ein Merkmal zu verwenden, um Funktionalität für eine einzelne Klasse bereitzustellen: Dies verschleiert nur, was die Klasse tut, und ein besseres Design würde die Funktionalität des Merkmals in die relevante Klasse verschieben.

Betrachten Sie die folgende Implementierung von Merkmalen:

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!';
    }
}

Ein konkreteres Beispiel: Stellen Sie sich vor, Sie FileCacherund Ihre ApcCacherMitarbeiter aus der Schnittstellendiskussion verwenden dieselbe Methode, um festzustellen, ob ein Cache-Eintrag veraltet ist und gelöscht werden sollte (dies ist im wirklichen Leben offensichtlich nicht der Fall, aber machen Sie mit). Sie können ein Merkmal schreiben und beiden Klassen erlauben, es für die allgemeine Schnittstellenanforderung zu verwenden.

Ein letztes Wort zur Vorsicht: Achten Sie darauf, dass Sie nicht mit Merkmalen über Bord gehen. Oft werden Merkmale als Krücke für schlechtes Design verwendet, wenn eindeutige Klassenimplementierungen ausreichen würden. Sie sollten Merkmale darauf beschränken, die Schnittstellenanforderungen für das beste Code-Design zu erfüllen.


69
Ich habe wirklich nach der schnellen, einfachen Antwort gesucht, die oben angegeben wurde, aber ich muss sagen, dass Sie eine ausgezeichnete ausführliche Antwort gegeben haben, die dazu beiträgt, die Unterscheidung für andere klarer zu machen, ein dickes Lob.
Datguywhowanders

35
"[C] Merkmale, die die für eine Schnittstelle in einer bestimmten Klasse erforderlichen Fähigkeiten erfüllen, sind ein idealer Anwendungsfall." Genau: +1
Alec Gorge

5
Wäre es fair zu sagen, dass Merkmale in PHP Mixins in anderen Sprachen ähnlich sind?
Eno

5
@igorpan Für alle Absichten und Zwecke würde ich sagen , PHP Merkmal Implementierung ist die gleiche wie Mehrfachvererbung. Es ist erwähnenswert, dass, wenn ein Merkmal in PHP statische Eigenschaften angibt, jede Klasse, die das Merkmal verwendet, eine eigene Kopie der statischen Eigenschaft hat. Noch wichtiger ist ... da dieser Beitrag jetzt bei den SERPs extrem hoch ist, wenn nach Merkmalen gefragt wird, werde ich oben auf der Seite eine öffentliche Bekanntmachung hinzufügen. Du solltest es lesen.
Rdlowrey

3
+1 für eine ausführliche Erklärung. Ich komme aus einem rubinroten Hintergrund, wo Mixins viel verwendet werden; Nur um meine zwei Cent hinzuzufügen, könnte eine gute Faustregel, die wir verwenden, in PHP übersetzt werden als "Implementiere keine Methoden, die dies in Merkmalen mutieren". Dies verhindert eine ganze Reihe verrückter Debugging-Sitzungen ... Ein Mixin sollte auch KEINE Annahmen über die Klasse treffen, in die es gemischt wird (oder Sie sollten es sehr deutlich machen und die Abhängigkeiten auf ein Minimum reduzieren). In dieser Hinsicht finde ich Ihre Vorstellung von Merkmalen, die Schnittstellen implementieren, gut.
m_x

67

A traitist im Wesentlichen die PHP-Implementierung von a mixinund ist effektiv eine Reihe von Erweiterungsmethoden, die durch Hinzufügen von zu jeder Klasse hinzugefügt werden können trait. Die Methoden werden dann Teil der Implementierung dieser Klasse, jedoch ohne Vererbung .

Aus dem PHP-Handbuch (Schwerpunkt Mine):

Merkmale sind ein Mechanismus zur Wiederverwendung von Code in einzelnen Vererbungssprachen wie PHP. ... Es ist eine Ergänzung zur traditionellen Vererbung und ermöglicht eine horizontale Zusammensetzung des Verhaltens. das heißt, die Anwendung von Klassenmitgliedern ohne Vererbung.

Ein Beispiel:

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

Mit dem oben definierten Merkmal kann ich jetzt Folgendes tun:

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

Wenn ich zu diesem Zeitpunkt eine Klasseninstanz erstelle MyClass, gibt es zwei Methoden, foo()und und bar()-, die von kommen myTrait. Und - beachten Sie, dass die trait-defined-Methoden bereits einen Methodenkörper haben - was eine Interface-defined-Methode nicht kann.

Darüber hinaus verwendet PHP wie viele andere Sprachen ein einzelnes Vererbungsmodell. Dies bedeutet, dass eine Klasse von mehreren Schnittstellen abgeleitet werden kann, jedoch nicht von mehreren Klassen. Eine PHP-Klasse kann jedoch mehrere traitEinschlüsse haben, wodurch der Programmierer wiederverwendbare Teile einschließen kann, wie dies bei mehreren Basisklassen der Fall sein könnte.

Ein paar Dinge zu beachten:

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

Polymorphismus:

Im vorherigen Beispiel, wo MyClass erweitert SomeBaseClass , MyClass ist eine Instanz von SomeBaseClass. Mit anderen Worten, ein Array wie SomeBaseClass[] baseskann Instanzen von enthalten MyClass. Und falls MyClasserweitert IBaseInterface, ein Array aus IBaseInterface[] baseskönnte Instanzen enthalten MyClass. Mit a ist kein solches polymorphes Konstrukt verfügbar, traitda a traitim Wesentlichen nur Code ist, der zur Vereinfachung für den Programmierer in jede Klasse kopiert wird, die ihn verwendet.

Vorrang:

Wie im Handbuch beschrieben:

Ein von einer Basisklasse geerbtes Mitglied wird von einem durch ein Merkmal eingefügten Mitglied überschrieben. Die Rangfolge lautet, dass Mitglieder der aktuellen Klasse Trait-Methoden überschreiben, die im Gegenzug geerbte Methoden überschreiben.

Stellen Sie sich also das folgende Szenario vor:

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 */ }
}

Beim Erstellen einer Instanz von MyClass (siehe oben) tritt Folgendes auf:

  1. Dies Interface IBaseerfordert SomeMethod()die Bereitstellung einer parameterlosen Funktion .
  2. Die Basisklasse BaseClassbietet eine Implementierung dieser Methode, um die Anforderungen zu erfüllen.
  3. Das trait myTraitbietet auch eine parameterlose Funktion SomeMethod(), die Vorrang vor der BaseClass-version hat
  4. Das class MyClassbietet eine eigene Version von SomeMethod()-, die Vorrang vor der trait-version hat.

Fazit

  1. Eine Interfacekann keine Standardimplementierung eines Methodenkörpers bereitstellen, während eine traitDose.
  2. An Interfaceist ein polymorphes , geerbtes Konstrukt - a traitnicht.
  3. Mehrere Interfaces kann in der gleichen Klasse verwendet werden, und so kann mehrere traits.

4
"Ein Merkmal ähnelt dem C # -Konzept einer abstrakten Klasse." Nein, eine abstrakte Klasse ist eine abstrakte Klasse. Dieses Konzept existiert sowohl in PHP als auch in C #. Ich würde stattdessen ein Merkmal in PHP mit einer statischen Klasse vergleichen, die aus Erweiterungsmethoden in C # besteht, wobei die typbasierte Einschränkung, die als Merkmal entfernt wurde, von so ziemlich jedem Typ verwendet werden kann, im Gegensatz zu einer Erweiterungsmethode, die nur einen Typ erweitert.
BoltClock

1
Sehr guter Kommentar - und ich stimme Ihnen zu. Beim erneuten Lesen ist das eine bessere Analogie. Ich glaube jedoch, dass es noch besser ist, es als ein zu betrachten mixin- und als ich die Eröffnung meiner Antwort noch einmal durchgesehen habe, habe ich aktualisiert, um dies widerzuspiegeln. Danke für den Kommentar, @BoltClock!
Troy Alford

1
Ich glaube nicht, dass es einen Zusammenhang mit c # -Erweiterungsmethoden gibt. Erweiterungsmethoden werden dem einzelnen Klassentyp hinzugefügt (natürlich unter Berücksichtigung der Klassenhierarchie). Ihr Zweck besteht darin, einen Typ mit zusätzlichen Funktionen zu erweitern, nicht Code über mehrere Klassen hinweg zu "teilen" und ein Durcheinander zu verursachen. Es ist nicht vergleichbar! Wenn etwas wiederverwendet werden muss, bedeutet dies normalerweise, dass es über einen eigenen Speicherplatz verfügen sollte, z. B. eine separate Klasse, die sich auf die Klassen bezieht, die gemeinsame Funktionen benötigen. Die Implementierung kann je nach Design variieren, aber ungefähr ist das so. Eigenschaften sind nur ein weiterer Weg, um einen schlechten Code zu erstellen.
Sofija

Eine Klasse kann mehrere Schnittstellen haben? Ich bin nicht sicher, ob ich Ihr Diagramm falsch verstehe, aber Klasse X implementiert Y, Z ist gültig.
Yann Chabot

26

Ich denke, traitses ist nützlich, Klassen zu erstellen, die Methoden enthalten, die als Methoden mehrerer verschiedener Klassen verwendet werden können.

Zum Beispiel:

trait ToolKit
{
    public $errors = array();

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

Sie können diese "Fehler" -Methode in jeder Klasse verwenden, die dieses Merkmal verwendet.

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
    }
}

Während mit können interfacesSie nur die Methodensignatur deklarieren, nicht aber den Funktionscode. Um eine Schnittstelle zu verwenden, müssen Sie außerdem einer Hierarchie folgen implements. Dies ist bei Merkmalen nicht der Fall.

Es ist ganz anders!


Ich denke, das ist ein schlechtes Beispiel für ein Merkmal. to_integerwäre eher in einer IntegerCastSchnittstelle enthalten, da es keinen grundsätzlich ähnlichen Weg gibt, Klassen (intelligent) in eine Ganzzahl umzuwandeln.
Matthew

5
Vergiss "to_integer" - es ist nur eine Illustration. Ein Beispiel. Eine "Hallo Welt". Ein "example.com".
J. Bruni

2
Welchen Vorteil bietet dieses Toolkit-Merkmal, das eine eigenständige Dienstprogrammklasse nicht bieten kann? Anstelle von use ToolkitIhnen könnte $this->toolkit = new Toolkit();oder vermisse ich einen Vorteil des Merkmals selbst?
Anthony

@Anthony irgendwo in Ihrem SomethingContainer, den Sie tunif(!$something->do_something('foo')) var_dump($something->errors);
TheRealChx101

20

Für Anfänger kann die obige Antwort schwierig sein. Dies ist der einfachste Weg, sie zu verstehen:

Züge

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

Wenn Sie also sayHelloFunktionen in anderen Klassen haben möchten, ohne die gesamte Funktion neu zu erstellen, können Sie Merkmale verwenden.

class MyClass{
  use SayWorld;

}

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

Cool richtig!

Nicht nur Funktionen, die Sie im Merkmal verwenden können (Funktion, Variablen, Konst.). Sie können auch mehrere Merkmale verwenden:use SayWorld,AnotherTraits;

Schnittstelle

  interface SayWorld {
     public function sayHello();
  }

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

So unterscheidet sich die Schnittstelle von den Merkmalen: Sie müssen alles in der Schnittstelle in der implementierten Klasse neu erstellen. Schnittstelle hat keine Implementierung. und die Schnittstelle kann nur Funktionen und const haben, sie kann keine Variablen haben.

Ich hoffe das hilft!


5

Eine häufig verwendete Metapher zur Beschreibung von Merkmalen ist, dass Merkmale Schnittstellen zur Implementierung sind.

Dies ist in den meisten Fällen eine gute Art, darüber nachzudenken, aber es gibt eine Reihe subtiler Unterschiede zwischen den beiden.

Zunächst einmal arbeitet der instanceofOperator nicht mit Merkmalen (dh ein Merkmal ist kein reales Objekt), sodass Sie uns nicht mitteilen können, ob eine Klasse ein bestimmtes Merkmal aufweist (oder ob zwei ansonsten nicht verwandte Klassen ein Merkmal gemeinsam haben) ). Das ist es, was sie damit meinen, dass es ein Konstrukt für die Wiederverwendung von horizontalem Code ist.

In PHP gibt es jetzt Funktionen, mit denen Sie eine Liste aller von einer Klasse verwendeten Merkmale abrufen können. Die Vererbung von Merkmalen bedeutet jedoch, dass Sie rekursive Überprüfungen durchführen müssen, um zuverlässig zu überprüfen, ob eine Klasse irgendwann ein bestimmtes Merkmal aufweist (es gibt ein Beispiel) Code auf den PHP-Doku-Seiten). Aber ja, es ist sicherlich nicht so einfach und sauber wie eine Instanz davon, und meiner Meinung nach ist es eine Funktion, die PHP besser machen würde.

Außerdem sind abstrakte Klassen immer noch Klassen, sodass sie keine Probleme bei der Wiederverwendung von Code mit Mehrfachvererbung lösen. Denken Sie daran, dass Sie nur eine Klasse (real oder abstrakt) erweitern, aber mehrere Schnittstellen implementieren können.

Ich habe festgestellt, dass Merkmale und Schnittstellen sehr gut Hand in Hand verwendet werden können, um eine Pseudo-Mehrfachvererbung zu erstellen. Z.B:

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

Auf diese Weise können Sie mithilfe von instanceof feststellen, ob das jeweilige Door-Objekt mit einem Schlüssel versehen ist oder nicht. Sie wissen, dass Sie einen konsistenten Satz von Methoden usw. erhalten und dass sich der gesamte Code in allen Klassen, die das KeyedTrait verwenden, an einer Stelle befindet.


Der letzte Teil dieser Antwort ist natürlich das, was @rdlowrey in den letzten drei Absätzen unter "Eigenschaften" in seinem Beitrag ausführlicher sagt. Ich hatte nur das Gefühl, dass ein wirklich einfaches Skelett-Code-Snippet helfen würde, es zu veranschaulichen.
Jon Kloske

Ich denke, die beste OO-Methode zur Verwendung von Merkmalen ist die Verwendung von Schnittstellen, wo Sie können. Und wenn es einen Fall gibt, in dem mehrere Unterklassen dieselbe Art von Code für diese Schnittstelle implementieren und Sie diesen Code nicht in ihre (abstrakte) Oberklasse verschieben können -> implementieren Sie ihn mit Merkmalen
Spieler-eins

4

Merkmale dienen lediglich der Wiederverwendung von Code .

Die Schnittstelle liefert lediglich die Signatur der Funktionen, die in der Klasse definiert werden sollen, in der sie je nach Ermessen des Programmierers verwendet werden kann . So erhalten wir einen Prototyp für eine Gruppe von Klassen .

Als Referenz http://www.php.net/manual/en/language.oop5.traits.php


3

Sie können ein Merkmal grundsätzlich als automatisiertes "Kopieren und Einfügen" von Code betrachten.

Die Verwendung von Merkmalen ist gefährlich, da es keine Möglichkeit gibt, vor der Ausführung zu wissen, was sie tun.

Merkmale sind jedoch flexibler, da sie keine Einschränkungen wie Vererbung aufweisen.

Merkmale können nützlich sein, um eine Methode einzufügen, die etwas in eine Klasse eincheckt, z. B. das Vorhandensein einer anderen Methode oder eines anderen Attributs. Ein schöner Artikel dazu (aber auf Französisch, sorry) .

Für französisch lesende Menschen, die es bekommen können, hat das GNU / Linux-Magazin HS 54 einen Artikel zu diesem Thema.



@ denis631 Sie können Merkmale als Codeausschnitte und Schnittstellen als Signaturverträge anzeigen. Wenn es helfen kann, können Sie es als informelles Teil einer Klasse betrachten, die alles enthalten kann. Lassen Sie mich wissen, ob es hilft.
Benj

Ich sehe, dass PHP-Merkmale als Makros angesehen werden können, die dann zur Kompilierungszeit erweitert werden / nur das Code-Snippet mit diesem Schlüssel aliasen. Rostmerkmale erscheinen jedoch anders (oder irre ich mich). Aber da beide Wortmerkmale enthalten, würde ich davon ausgehen, dass sie gleich sind, was dasselbe Konzept bedeutet. Link zu Rostmerkmalen
denis631

2

Wenn Sie Englisch sprechen und wissen, was es traitbedeutet, ist es genau das, was der Name sagt. Es handelt sich um ein klassenloses Paket von Methoden und Eigenschaften, die Sie durch Eingabe an vorhandene Klassen anhängen use.

Grundsätzlich können Sie es mit einer einzelnen Variablen vergleichen. Closures-Funktionen können usediese Variablen von außerhalb des Gültigkeitsbereichs verwenden und haben auf diese Weise den Wert innerhalb. Sie sind mächtig und können in allem eingesetzt werden. Gleiches passiert mit Merkmalen, wenn sie verwendet werden.


2

Andere Antworten haben die Unterschiede zwischen Schnittstellen und Merkmalen hervorragend erklärt. Ich werde mich auf ein nützliches Beispiel aus der realen Welt konzentrieren, insbesondere eines, das zeigt, dass Merkmale Instanzvariablen verwenden können, sodass Sie einer Klasse mit minimalem Code für das Boilerplate Verhalten hinzufügen können.

Wie bereits von anderen erwähnt, lassen sich Merkmale gut mit Schnittstellen kombinieren, sodass die Schnittstelle den Verhaltensvertrag und das Merkmal zur Erfüllung der Implementierung angeben kann.

Das Hinzufügen von Funktionen zum Veröffentlichen / Abonnieren von Ereignissen zu einer Klasse kann in einigen Codebasen ein häufiges Szenario sein. Es gibt 3 gängige Lösungen:

  1. Definieren Sie eine Basisklasse mit Ereignis-Pub / Sub-Code. Klassen, die Ereignisse anbieten möchten, können diese erweitern, um die Funktionen zu nutzen.
  2. Definieren Sie eine Klasse mit Ereignis-Pub / Sub-Code, und andere Klassen, die Ereignisse anbieten möchten, können diese über die Komposition verwenden, indem sie ihre eigenen Methoden zum Umschließen des zusammengesetzten Objekts definieren und die Methodenaufrufe an dieses weiterleiten.
  3. Definieren Sie ein Merkmal mit Ereignis-Pub / Sub-Code, und dann können andere Klassen, die Ereignisse anbieten möchten, usedas Merkmal, auch bekannt als Import, verwenden, um die Funktionen zu erhalten.

Wie gut funktioniert jeder?

# 1 Funktioniert nicht gut. Bis zu dem Tag, an dem Sie feststellen, dass Sie die Basisklasse nicht erweitern können, weil Sie bereits etwas anderes erweitern. Ich werde kein Beispiel dafür zeigen, da es offensichtlich sein sollte, wie einschränkend es ist, eine solche Vererbung zu verwenden.

# 2 & # 3 funktionieren beide gut. Ich werde ein Beispiel zeigen, das einige Unterschiede hervorhebt.

Zunächst ein Code, der zwischen beiden Beispielen gleich ist:

Eine Schnittstelle

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

Und etwas Code, um die Verwendung zu demonstrieren:

$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());
}

Ok, jetzt zeigen wir, wie sich die Implementierung der AuctionKlasse bei der Verwendung von Merkmalen unterscheidet.

Hier ist zunächst, wie # 2 (mit Komposition) aussehen würde:

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);
    }
}

So würde # 3 (Merkmale) aussehen:

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]);
    }
}

Beachten Sie, dass der Code in der EventEmitterTraitDatei genau mit dem in der EventEmitterKlasse übereinstimmt, außer dass das Merkmal die triggerEvent()Methode als geschützt deklariert . Also, der einzige Unterschied , den Sie brauchen , zu betrachten ist die Implementierung der AuctionKlasse .

Und der Unterschied ist groß. Wenn wir Komposition verwenden, erhalten wir eine großartige Lösung, die es uns ermöglicht, unsere EventEmittervon so vielen Klassen wiederzuverwenden , wie wir möchten. Der Hauptnachteil ist jedoch, dass wir viel Boilerplate-Code haben, den wir schreiben und pflegen müssen, da wir ihn für jede in der ObservableSchnittstelle definierte Methode implementieren und langweiligen Boilerplate-Code schreiben müssen, der die Argumente nur an die entsprechende Methode in weiterleitet Wir haben das EventEmitterObjekt komponiert . Durch die Verwendung des Merkmals in diesem Beispiel können wir dies vermeiden und den Code für die Boilerplate reduzieren und die Wartbarkeit verbessern .

Es kann jedoch vorkommen, dass Ihre AuctionKlasse nicht die vollständige ObservableSchnittstelle implementieren soll. Vielleicht möchten Sie nur 1 oder 2 Methoden oder gar keine verfügbar machen, damit Sie Ihre eigenen Methodensignaturen definieren können. In einem solchen Fall bevorzugen Sie möglicherweise immer noch die Kompositionsmethode.

Das Merkmal ist jedoch in den meisten Szenarien sehr überzeugend, insbesondere wenn die Schnittstelle viele Methoden enthält, was dazu führt, dass Sie viele Boilerplates schreiben.

* Sie könnten tatsächlich beides tun - definieren Sie die EventEmitterKlasse, falls Sie sie jemals kompositorisch verwenden möchten, und definieren Sie das EventEmitterTraitMerkmal auch mithilfe der EventEmitterKlassenimplementierung innerhalb des Merkmals :)


1

Das Merkmal ist dasselbe wie eine Klasse, die wir für mehrere Vererbungszwecke und auch für die Wiederverwendbarkeit von Code verwenden können.

Wir können Merkmale innerhalb der Klasse verwenden und wir können auch mehrere Merkmale in derselben Klasse mit 'Schlüsselwort verwenden' verwenden.

Die Schnittstelle wird für die Wiederverwendbarkeit von Code wie ein Merkmal verwendet

Die Schnittstelle besteht aus mehreren Schnittstellen, damit wir die Probleme mit der Mehrfachvererbung lösen können. Wenn wir jedoch die Schnittstelle implementieren, sollten wir alle Methoden innerhalb der Klasse erstellen. Für weitere Informationen klicken Sie auf den folgenden Link:

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



0

Der Hauptunterschied besteht darin, dass Sie bei Schnittstellen die tatsächliche Implementierung jeder Methode in jeder Klasse definieren müssen, die diese Schnittstelle implementiert, sodass viele Klassen dieselbe Schnittstelle implementieren können, jedoch mit unterschiedlichem Verhalten, während Merkmale nur eingebaute Codestücke sind eine Klasse; Ein weiterer wichtiger Unterschied besteht darin, dass Merkmalsmethoden nur Klassenmethoden oder statische Methoden sein können, im Gegensatz zu Schnittstellenmethoden, die auch Instanzmethoden sein können (und normalerweise sind).

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.