Altre risposte hanno fatto un ottimo lavoro nel spiegare le differenze tra interfacce e tratti. Mi concentrerò su un utile esempio del mondo reale, in particolare uno che dimostra che i tratti possono usare le variabili di istanza, permettendoti di aggiungere un comportamento a una classe con un minimo codice di boilerplate.
Ancora una volta, come menzionato da altri, i tratti si accoppiano bene con le interfacce, consentendo all'interfaccia di specificare il contratto di comportamento e il tratto per realizzare l'implementazione.
L'aggiunta di funzionalità di pubblicazione / sottoscrizione di eventi a una classe può essere uno scenario comune in alcune basi di codice. Esistono 3 soluzioni comuni:
- Definire una classe di base con pub / sottocodice di eventi, quindi le classi che desiderano offrire eventi possono estenderla per ottenere le funzionalità.
- Definire una classe con pub / sottocodice di eventi, quindi altre classi che vogliono offrire eventi possono usarlo tramite composizione, definendo i propri metodi per avvolgere l'oggetto composto, inoltrando il metodo che chiama.
- Definisci un tratto con pub / sottocodice di eventi, quindi altre classi che vogliono offrire eventi possono
use
trarre il tratto, ovvero importarlo, per ottenere le funzionalità.
Quanto bene funziona ciascuno?
# 1 Non funziona bene. Sarebbe, fino al giorno in cui ti rendi conto che non puoi estendere la classe base perché stai già estendendo qualcos'altro. Non mostrerò un esempio di questo perché dovrebbe essere ovvio quanto sia limitativo usare l'eredità in questo modo.
# 2 e # 3 funzionano entrambi bene. Mostrerò un esempio che evidenzia alcune differenze.
Innanzitutto, un po 'di codice che sarà lo stesso tra entrambi gli esempi:
Un'interfaccia
interface Observable {
function addEventListener($eventName, callable $listener);
function removeEventListener($eventName, callable $listener);
function removeAllEventListeners($eventName);
}
E un po 'di codice per dimostrare l'utilizzo:
$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, ora mostriamo come l'implementazione della Auction
classe differirà quando si usano i tratti.
Innanzitutto, ecco come sarebbe il n. 2 (usando la composizione):
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);
}
}
Ecco come sarebbe il n. 3 (tratti):
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]);
}
}
Si noti che il codice all'interno di EventEmitterTrait
è esattamente lo stesso di ciò che è all'interno della EventEmitter
classe tranne il tratto dichiara il triggerEvent()
metodo come protetto. Quindi, l'unica differenza che devi guardare è l'implementazione della Auction
classe .
E la differenza è grande. Quando usiamo la composizione, otteniamo un'ottima soluzione, permettendoci di riutilizzare le nostre EventEmitter
classi quante ne vogliamo. Ma il principale svantaggio è che abbiamo un sacco di codice boilerplate che dobbiamo scrivere e mantenere perché per ogni metodo definito Observable
nell'interfaccia, dobbiamo implementarlo e scrivere noioso codice boilerplate che inoltra gli argomenti sul metodo corrispondente in abbiamo composto l' EventEmitter
oggetto. L'uso del tratto in questo esempio ci consente di evitarlo , aiutandoci a ridurre il codice della caldaia e migliorare la manutenibilità .
Tuttavia, ci possono essere momenti in cui non vuoi che la tua Auction
classe implementi l' Observable
interfaccia completa - forse vuoi solo esporre 1 o 2 metodi, o forse addirittura nessuno, in modo da poter definire le tue firme dei metodi. In tal caso, potresti comunque preferire il metodo di composizione.
Ma il tratto è molto interessante nella maggior parte degli scenari, specialmente se l'interfaccia ha molti metodi, il che ti fa scrivere un sacco di piatti.
* In realtà potresti fare entrambe le cose: definire la EventEmitter
classe nel caso in cui tu voglia usarla in modo compositivo e definire anche il EventEmitterTrait
tratto, usando l' EventEmitter
implementazione della classe all'interno del tratto :)