394 Stimmen

Eigenschaften vs. Schnittstellen

Ich habe in letzter Zeit versucht, mich in PHP einzuarbeiten, und bin immer wieder auf Traits gestoßen. Ich verstehe das Konzept der horizontalen Wiederverwendung von Code und dass man nicht unbedingt von einer abstrakten Klasse erben will. Was ich nicht verstehe, ist: Was ist der entscheidende Unterschied zwischen der Verwendung von Traits und Schnittstellen?

Ich habe versucht, einen vernünftigen Blogbeitrag oder Artikel zu finden, in dem erklärt wird, wann man das eine oder das andere verwenden sollte, aber die Beispiele, die ich bisher gefunden habe, scheinen so ähnlich zu sein, dass sie identisch sind.

8voto

Rajesh Paul Punkte 6284

Eigenschaften sind einfach für Code-Wiederverwendung .

Schnittstelle bietet lediglich die Unterschrift der Funktionen, die zu definiert in der Klasse wo sie verwendet werden kann, je nach Ermessensspielraum des Programmierers . Dies gibt uns eine Prototyp para eine Gruppe von Klassen .

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

6voto

Jon Marnock Punkte 2955

Eine häufig verwendete Metapher zur Beschreibung von Traits lautet: Traits sind Schnittstellen mit Implementierung.

Dies ist in den meisten Fällen eine gute Denkweise, aber es gibt eine Reihe von subtilen Unterschieden zwischen den beiden.

Zum einen ist die instanceof Operator funktioniert nicht mit Traits (d.h. ein Trait ist kein echtes Objekt), daher kann man ihn nicht verwenden, um zu sehen, ob eine Klasse eine bestimmte Eigenschaft hat (oder um zu sehen, ob zwei ansonsten nicht verwandte Klassen eine Eigenschaft teilen). Das ist es, was sie damit meinen, dass es ein Konstrukt für die horizontale Wiederverwendung von Code ist.

Dort sind Funktionen in PHP, mit denen Sie eine Liste aller Traits erhalten, die eine Klasse verwendet, aber Trait-Vererbung bedeutet, dass Sie rekursive Überprüfungen durchführen müssen, um zuverlässig zu prüfen, ob eine Klasse zu einem bestimmten Zeitpunkt einen bestimmten Trait hat (es gibt Beispielcode auf den PHP-Doku-Seiten). Aber ja, es ist sicherlich nicht so einfach und sauber wie instanceof ist, und IMHO ist es eine Funktion, die PHP besser machen würde.

Außerdem sind abstrakte Klassen immer noch Klassen, so dass sie keine Probleme mit der Wiederverwendung von Code im Zusammenhang 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 Traits und Schnittstellen wirklich gut sind, um Hand in Hand zu verwenden, um Pseudo-Mehrfachvererbung zu erstellen. Beispiel:

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 instanceof um festzustellen, ob ein bestimmtes Door-Objekt Keyed ist oder nicht, können Sie sicher sein, dass Sie einen konsistenten Satz von Methoden usw. erhalten, und der gesamte Code befindet sich in allen Klassen, die das KeyedTrait verwenden, an einer Stelle.

4voto

Benj Punkte 1126

Ein Trait ist im Grunde genommen ein automatisiertes "Copy-Paste" von Code.

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

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

Traits können nützlich sein, um eine Methode in eine Klasse einzuschleusen, die etwas überprüft, z. B. die Existenz einer anderen Methode oder eines Attributs. Ein schöner Artikel dazu (aber leider auf Französisch) .

Für alle, die Französisch lesen können, hat das GNU/Linux Magazin HS 54 einen Artikel zu diesem Thema.

2voto

Thielicious Punkte 3590

Wenn Sie Englisch können und wissen, was trait bedeutet, dass es genau das ist, was der Name sagt. Es handelt sich um ein klassenloses Paket von Methoden und Eigenschaften, die Sie an bestehende Klassen anhängen, indem Sie Folgendes eingeben use .

Im Grunde könnte man sie mit einer einzigen Variablen vergleichen. Closures-Funktionen können use diese Variablen von außerhalb des Geltungsbereichs, so dass sie den Wert innerhalb haben. Sie sind mächtig und können in allem verwendet werden. Dasselbe geschieht mit Traits, wenn sie verwendet werden.

2voto

goat Punkte 30238

Andere Antworten erklärten sehr gut die Unterschiede zwischen Schnittstellen und Merkmalen. Ich werde mich auf ein nützliches Beispiel aus der realen Welt konzentrieren, insbesondere auf eines, das zeigt, dass Traits Instanzvariablen verwenden können, wodurch Sie einer Klasse mit minimalem Boilerplate-Code Verhalten hinzufügen können.

Wie bereits von anderen erwähnt, lassen sich Traits gut mit Schnittstellen kombinieren, so dass die Schnittstelle den Verhaltenskontrakt spezifizieren kann und der Trait die Implementierung übernimmt.

Das Hinzufügen von Ereignisveröffentlichungs- und -abonnementfunktionen zu einer Klasse kann in einigen Codebasen ein gängiges Szenario sein. Es gibt 3 gängige Lösungen:

  1. Definieren Sie eine Basisklasse mit Ereignis-Pub/Sub-Code, und dann können Klassen, die Ereignisse anbieten wollen, diese erweitern, um die Fähigkeiten zu erhalten.
  2. Definieren Sie eine Klasse mit Ereignis-Pub/Sub-Code, und dann können andere Klassen, die Ereignisse anbieten wollen, sie über Komposition verwenden, indem sie ihre eigenen Methoden definieren, um das komponierte Objekt zu umhüllen, und die Methodenaufrufe an sie weiterleiten.
  3. Definieren Sie einen Trait mit Ereignis-Pub/Sub-Code, und dann können andere Klassen, die Ereignisse anbieten wollen use die Eigenschaft, d.h. importieren, um die Fähigkeiten zu erhalten.

Wie gut funktionieren beide?

1 Funktioniert nicht gut. Doch, 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, weil es offensichtlich sein sollte, wie einschränkend es ist, Vererbung auf diese Weise zu verwenden.

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

Zunächst etwas Code, der in beiden Beispielen gleich sein wird:

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 wollen wir zeigen, wie die Implementierung der Auction Klasse wird sich bei der Verwendung von Traits unterscheiden.

Erstens: So würde die Nummer 2 (mit Komposition) aussehen:

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 innerhalb der EventEmitterTrait ist genau dasselbe wie das, was in der EventEmitter Klasse, außer dass der Trait die triggerEvent() Methode als geschützt. Also, der einzige Unterschied, den Sie beachten müssen, ist die Implementierung der Auction Klasse .

Und der Unterschied ist groß. Wenn wir die Komposition verwenden, erhalten wir eine großartige Lösung, die es uns ermöglicht, unsere Daten wiederzuverwenden. EventEmitter von so vielen Klassen, wie wir wollen. Der größte Nachteil ist jedoch, dass wir eine Menge Standardcode schreiben und pflegen müssen, da für jede Methode, die in der Observable Schnittstelle, müssen wir sie implementieren und einen langweiligen Standardcode schreiben, der die Argumente einfach an die entsprechende Methode in unserer zusammengesetzten Methode weiterleitet. EventEmitter Objekt. Verwendung von Mit der Eigenschaft in diesem Beispiel können wir das vermeiden und hilft uns Reduzierung von Standardcode und Verbesserung der Wartungsfreundlichkeit .

Es kann jedoch vorkommen, dass Sie nicht möchten, dass Ihr Auction Klasse zur Implementierung der vollständigen Observable Schnittstelle - vielleicht wollen Sie nur 1 oder 2 Methoden oder vielleicht sogar gar keine, so dass Sie Ihre eigenen Methodensignaturen definieren können. In einem solchen Fall könnten Sie immer noch die Kompositionsmethode bevorzugen.

Aber der Trait ist in den meisten Szenarien sehr überzeugend, besonders wenn die Schnittstelle viele Methoden hat, was dazu führt, dass man viel Boilerplate schreiben muss.

* Man könnte eigentlich beides machen - die EventEmitter Klasse, falls Sie sie jemals kompositorisch verwenden wollen, und definieren Sie die EventEmitterTrait auch mit der Eigenschaft EventEmitter Klassenimplementierung innerhalb des Traits :)

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X