D'autres réponses ont fait un excellent travail pour expliquer les différences entre les interfaces et les traits. Je me concentrerai sur un exemple utile du monde réel, en particulier celui qui démontre que les traits peuvent utiliser des variables d'instance - vous permettant d'ajouter un comportement à une classe avec un code passe-partout minimal.
Encore une fois, comme mentionné par d'autres, les traits s'associent bien avec les interfaces, permettant à l'interface de spécifier le contrat de comportement et le trait de remplir l'implémentation.
L'ajout de capacités de publication / abonnement d'événement à une classe peut être un scénario courant dans certaines bases de code. Il existe 3 solutions communes:
- Définissez une classe de base avec le code pub / sous-événement, puis les classes qui souhaitent proposer des événements peuvent l'étendre afin d'acquérir les capacités.
- Définissez une classe avec le code pub / sous-événement, puis les autres classes qui souhaitent proposer des événements peuvent l'utiliser via la composition, en définissant leurs propres méthodes pour encapsuler l'objet composé, en mandatant les appels de méthode.
- Définissez un trait avec le code pub / sous-événement, puis les autres classes qui souhaitent proposer des événements peuvent
use
le trait, alias l'importer, pour gagner les capacités.
Dans quelle mesure chacun fonctionne-t-il?
# 1 Ne fonctionne pas bien. Ce serait le cas, jusqu'au jour où vous vous rendez compte que vous ne pouvez pas étendre la classe de base parce que vous étendez déjà autre chose. Je ne montrerai pas d'exemple de cela, car il devrait être évident à quel point il est limité d'utiliser l'héritage comme celui-ci.
# 2 & # 3 fonctionnent bien. Je vais montrer un exemple qui met en évidence certaines différences.
Tout d'abord, du code qui sera le même entre les deux exemples:
Une interface
interface Observable {
function addEventListener($eventName, callable $listener);
function removeEventListener($eventName, callable $listener);
function removeAllEventListeners($eventName);
}
Et du code pour démontrer l'utilisation:
$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, permet maintenant de montrer comment l'implémentation de la Auction
classe différera lors de l'utilisation des traits.
Tout d'abord, voici à quoi ressemblerait le # 2 (en utilisant la composition):
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);
}
}
Voici à quoi ressemblerait le # 3 (traits):
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]);
}
}
Notez que le code à l'intérieur de la EventEmitterTrait
est exactement le même que celui de la EventEmitter
classe, sauf que le trait déclare la triggerEvent()
méthode protégée. Ainsi, la seule différence que vous devez considérer est l'implémentation de la Auction
classe .
Et la différence est grande. Lorsque nous utilisons la composition, nous obtenons une excellente solution, nous permettant de réutiliser notre EventEmitter
par autant de classes que nous le souhaitons. Mais, le principal inconvénient est que nous avons beaucoup de code passe-partout que nous devons écrire et maintenir, car pour chaque méthode définie dans l' Observable
interface, nous devons l'implémenter et écrire un code passe-partout ennuyeux qui transmet simplement les arguments à la méthode correspondante dans notre composé l' EventEmitter
objet. L'utilisation de la caractéristique dans cet exemple nous permet d'éviter cela , ce qui nous aide à réduire le code standard et à améliorer la maintenabilité .
Cependant, il peut y avoir des moments où vous ne voulez pas que votre Auction
classe implémente l' Observable
interface complète - peut-être que vous voulez seulement exposer 1 ou 2 méthodes, ou peut-être même aucune pour que vous puissiez définir vos propres signatures de méthode. Dans un tel cas, vous préférerez peut-être toujours la méthode de composition.
Mais, le trait est très convaincant dans la plupart des scénarios, surtout si l'interface a beaucoup de méthodes, ce qui vous oblige à écrire beaucoup de passe-partout.
* Vous pouvez en fait faire les deux - définir la EventEmitter
classe au cas où vous voudriez l'utiliser de manière compositionnelle, et définir le EventEmitterTrait
trait aussi, en utilisant l' EventEmitter
implémentation de classe à l'intérieur du trait :)