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:
- 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.
- 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.
- 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 :)