Otras respuestas hicieron un gran trabajo al explicar las diferencias entre interfaces y rasgos. Me centraré en un ejemplo útil del mundo real, en particular uno que demuestre que los rasgos pueden usar variables de instancia, lo que le permite agregar comportamiento a una clase con un código mínimo repetitivo.
Nuevamente, como lo mencionaron otros, los rasgos se combinan bien con las interfaces, permitiendo que la interfaz especifique el contrato de comportamiento y el rasgo para cumplir con la implementación.
Agregar capacidades de publicación / suscripción de eventos a una clase puede ser un escenario común en algunas bases de código. Hay 3 soluciones comunes:
- Defina una clase base con código de publicación / sub de eventos, y luego las clases que desean ofrecer eventos pueden extenderla para obtener las capacidades.
- Defina una clase con el código de publicación / sub del evento, y luego otras clases que deseen ofrecer eventos pueden usarla a través de la composición, definiendo sus propios métodos para envolver el objeto compuesto, representando las llamadas al método.
- Defina un rasgo con el código de pub / sub del evento, y luego otras clases que deseen ofrecer eventos pueden
use
el rasgo, también conocido como importarlo, para obtener las capacidades.
¿Qué tan bien funciona cada uno?
# 1 no funciona bien. Lo haría, hasta el día en que te des cuenta de que no puedes extender la clase base porque ya estás extendiendo algo más. No mostraré un ejemplo de esto porque debería ser obvio cuán limitante es usar una herencia como esta.
# 2 y # 3 ambos funcionan bien. Mostraré un ejemplo que resalta algunas diferencias.
Primero, un código que será el mismo entre ambos ejemplos:
Una interfaz
interface Observable {
function addEventListener($eventName, callable $listener);
function removeEventListener($eventName, callable $listener);
function removeAllEventListeners($eventName);
}
Y algo de código para demostrar el uso:
$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, ahora vamos a mostrar cómo la implementación de la Auction
clase diferirá al usar rasgos.
Primero, así es como se vería el # 2 (usando composición):
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);
}
}
Así es como se vería el # 3 (rasgos):
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]);
}
}
Tenga en cuenta que el código dentro de EventEmitterTrait
es exactamente el mismo que está dentro de la EventEmitter
clase, excepto que el rasgo declara que el triggerEvent()
método está protegido. Entonces, la única diferencia que debe observar es la implementación de la Auction
clase .
Y la diferencia es grande. Cuando usamos composición, obtenemos una gran solución, que nos permite reutilizar nuestras EventEmitter
clases tantas veces como queramos. Pero, el principal inconveniente es que tenemos una gran cantidad de código repetitivo que necesitamos escribir y mantener porque para cada método definido en la Observable
interfaz, necesitamos implementarlo y escribir código repetitivo aburrido que solo reenvíe los argumentos al método correspondiente en nuestro compuesto el EventEmitter
objeto. El uso del rasgo en este ejemplo nos permite evitar eso , lo que nos ayuda a reducir el código repetitivo y mejorar la capacidad de mantenimiento .
Sin embargo, puede haber ocasiones en las que no desee que su Auction
clase implemente la versión completaObservable
interfaz ; tal vez solo quiera exponer 1 o 2 métodos, o tal vez incluso ninguno para que pueda definir sus propias firmas de métodos. En tal caso, aún puede preferir el método de composición.
Pero, el rasgo es muy convincente en la mayoría de los escenarios, especialmente si la interfaz tiene muchos métodos, lo que hace que escribas muchas repeticiones.
* Realmente podrías hacer ambas cosas: define la EventEmitter
clase en caso de que alguna vez quieras usarla de forma compositiva y define el EventEmitterTrait
rasgo también, usando la EventEmitter
implementación de la clase dentro del rasgo :)