jueves, 8 de agosto de 2013

PHP orientado a objetos - Herencia múltiple

PHP al igual que Java u Objectice-C no soporta herencia múltiple. Ello se debe a que si se heredasen dos clases que contengan métodos con el mismo nombre no sabría que clase debería de tener preferencia.

Pero si estamos ante una situación en la que necesitamos herencia múltiple no está todo perdido.  Ya que existen dos métodos para conseguir simular este tipo de herencia. Aunque el segundo de ellos solo está disponible a partir de PHP 5.4.

Uso de interfaces para simular la herencia múltiple

Como comentamos en el tutorial sobre las interfaces, una clase que implementa una interfaz tiene disponible todos los métodos definidos. Además de estar obligada a implementarlos. Por lo que si una clase implementa una interfaz significa que puede hacer todo lo que la interfaz quiere que haga.

Y como también mencionamos, una clase puede implementar varias interfaces. Veamos un ejemplo:

 interface Barco {  //una interfaz solo puede tener métodos públicos
   function hundirse();  
   function atracar();  
   function desembarcar();  
 }  
 interface Avion {  
   function despegar();  
   function aterrizar();  
 }  
 class HidroAvion implements Barco, Avion {  
   public function aterrizar() {  
   }  
   public function atracar() {  
   }  
   public function desembarcar() {  
   }  
   public function despegar() {  
   }  
   public function hundirse() {  
   }  
 }  
 $hidro = new HidroAvion();  

Vemos que la clase HidroAvion debe de implementar todos los métodos que las interfaces definen. Es una especie de 'contrato'. Y es así como simulamos la herencia múltiple. Ya que el citado 'contrato' obligará a que tengamos las definiciones de los métodos especificados en las interfaces. Además de tener que implementarlos.

Es posible que implementar muchas interfaces en una clase haga el código difícil de leer. Por lo que es recomendable agrupar interfaces combinando para ello interfaces y herencia.

 interface Barco {  
   function hundirse();  
   function atracar();  
   function desembarcar();  
 }  
 interface Avion extends Barco {  
   function despegar();  
   function aterrizar();  
 }  
 class HidroAvion implements Avion {  
   public function aterrizar() {  
   }  
   public function atracar() {  
   }  
   public function desembarcar() {  
   }  
   public function despegar() {  
   }  
   public function hundirse() {  
   }  
 }  
 $hidro = new HidroAvion();  

Ahora la clase HidroAvion solo implementa una interfaz, Avion. Y Avion esta heredando la interfaz Barco. Por lo que cualquier clase que implemente Avion, también esta heredando la obligación de implementar los métodos de la interfaz Barco. Y esto es lo que hace HidroAvion.
Esto puede hacer que el código sea más legible cuando el número de clases que intervienen en la simulación de herencia múltiple es muy grande. Sin embargo, según se combine la herencia y las interfaces se puede llegar a perder semántica, como ocurre en el ejemplo. Ya que una entidad Avion está heredando de una entidad Barco. Y eso parece no tener mucho sentido semántico.

Uso de 'traits'

En la versión 5.4 de PHP aparece un nuevo mecanismo destinado a poder reutilizar código en un lenguaje que por su naturaleza no soporta la herencia múltiple (caso del PHP). Un 'trait' busca reducir las limitaciones de la herencia simple, permitiendo a los desarrolladores reutilizar un conjunto de métodos en varias clases independientes de jerarquías. Por ello se suele decir que permiten la reutilización horizontal de código. La herencia permite una reutilización vertical (jerarquía).
Los 'traits' no son exclusivos de PHP ya que están disponibles en otros lenguajes como SCALA, Perl, C#, Java o Python.

Definir un 'trait' es parecido a definir una clase con sus atributos (con cualquier visibilidad) y métodos. La diferencia es que un 'trait' no se puede instanciar y una clase no puede heredar de un 'trait'. Además sustituiremos la palabra reservada class por trait.

 trait Base{  
   private $saludo = "Hola mundo!!!!!!";  
   public function getSaludo(){  
     return $this->saludo;  
   }  
 }  

Y para usar este 'trait' simplemente lo llamaremos dentro de una clase mediante la palabra reservada use.

 class Ejemplo{  
   use Base;  
 }  
 $ej = new Ejemplo();  
 echo $ej->getSaludo();  

Pero lo más importante es que podemos llamar a más de un 'trait' mediante la palabra reservada use. Pudiendo de esta  forma simular la ansiada herencia múltiple.

 trait Base1  
 {  
   public function hola1($nombre)  
   {  
     return "Hola1: " . $nombre;  
   }  
 }   
 trait Base2  
 {  
   public function hola2($nombre)  
   {  
     return "Hola2: " . $nombre;  
   }  
 }   
 class Ejemplo2  
 {  
   use Base1, Base2;  
 }   
 $e = new Ejemplo2();  
 echo $e->hola1('Ivan') . "<br/>";  
 echo $e->hola2('Ivan') . "<br/>";  

1. Conflictos de nombres

A) Conflicto entre clase y 'trait'

Los 'traits' son muy útiles para simular la herencia múltiple que no esta presente en PHP. Pero no solo sirven para eso, ya que se pueden utilizar en cualquier situación en que necesitemos una reutilización de código. Sin embargo tenemos que tener cuidado cuando un 'trait' tiene un método llamado de la misma forma que un método presente en la clase que usa el citado 'trait'.

 trait MiTrait{  
   private function MiMetodo(){  
     echo "este es un método de MiTrait";  
   }  
 }  
 class MiClase{  
   use MiTrait;  
   public function MiMetodo(){  
     echo "este es un método de MiClase";  
   }  
 }  
 $c = new MiClase();  
 $c->MiMetodo();  // este es un método de MiClase

PHP resuelve la situación dando mayor precedencia al método de la clase actual que al método de un 'trait'. Independientemente de la visibilidad. Si ambas estructuras tienen un método llamado igual, PHP elige al de la clase.

B) Conflictos con la herencia simple

Nombres de método iguales también ocasionan conflictos en el caso que el 'trait' utilizado y la clase heredada tengan métodos llamados igual:

 trait MiTrait {  
   public function MiMetodo() {  
     echo "este es un método de MiTrait";  
   }  
 }  
 class UnaClase {  
   public function MiMetodo() {  
     echo "este es un método de MiClase que he heredado";  
   }  
 }  
 class MiClase extends UnaClase{  
   use MiTrait;  
 }  
 $c = new MiClase();  
 $c->MiMetodo();  //este es un método de MiTrait

En el apartado anterior se dijo que un método de una clase donde se usa un 'trait' tiene mayor precedencia que el método llamado del mismo modo de dicho 'trait'. Pero en el caso actual, un método de un 'trait' sobreescribe un método heredado.

C) Conflictos con la herencia múltiple

Anteriormente se ha comentado que PHP, al igual que otros lenguajes, no implementaban la herencia múltiple debido a que no sabe resolver problemas de precedencia que se ocasionarían si se permitiese heredar de dos clases con métodos llamados igual. No sabe que método tiene que usar.

Inicialmente con los 'traits' tenemos el mismo problema. Por ejemplo, vamos a llamar  a dos 'traits' que tienen funciones con el mismo nombre.

 trait Base1  
 {  
   public function hola1($nombre)  
   {  
     return "Hola1: {$nombre}";  
   }  
 }   
 trait Base2  
 {  
   public function hola1($nombre)  
   {  
     return "Hola2: {$nombre}";  
   }  
 }   
 class Ejemplo2  
 {  
   use Base1, Base2;  
 }   
 $e = new Ejemplo2();  
 echo $e->hola1('Ivan') . "<br/>";  

El navegador devolverá el siguiente error: "Fatal error: Trait method hola1 has not been applied, because there are collisions with other trait methods on Ejemplo2 ...". Por lo tanto PHP sigue sin saber que método escoger. Pero como hemos, dicho los 'traits' están pensados para que poder realizar (simular) herencia múltiple. Por lo tanto hay una solución para resolver los posibles problemas de colisiones con la herencia múltiple. Y para ello simplemente indicaremos que método vamos a usar utilizando el operador insteadof.
La sintaxis del operador es la siguiente:

 trait_método_preferencia :: nombre_método  insteadof  trait_método_no_preferencia  

En nuestro ejemplo indicaremos que queremos usar el método hola1 de la clase Base1 en lugar del método llamado del mismo modo en Base2

 class Ejemplo2 {  
   use Base1,Base2 {  
     Base1::hola1 insteadof Base2;  
   }  
 }  
 $e = new Ejemplo2();  
 echo $e->hola1('Ivan') . "<br/>";  

D) Alias

Con el anterior operador hemos solucionado los conflictos con los nombre. Ya que hemos elegido entre los dos métodos. Pero es posible que no queramos elegir. Ya que puede que cada método tenga una implementación diferente y queramos utilizar ambas. Para este caso hay otro operador que nos va a venir muy bien. El operador as nos permite usar un alias para uno de los métodos conflictivos. Y de esta forma podremos utilizarlo.
El uso del operador as es similar al de insteadof. la sintaxis es la siguiente:

 trait :: nombre_método as alias_método  

Evidentemente, si el alias lo aplicamos sobre un método que no ocasiona conflicto de nombre no hace falta especificar el 'trait' donde está definido.

Vamos a ver un ejemplo:

 trait Base1 {  
   public function hola($nombre) {  
     return "Hola1: {$nombre}";  
   }  
   public function adios($nombre){  
     return "Adios1: {$nombre}";  
   }  
 }  
 trait Base2 {  
   public function hola($nombre) {  
     return "Hola2: {$nombre}";  
   }  
   public function adios($nombre){  
     return "Adios2: {$nombre}";  
   }  
 }  
 class Ejemplo2 {  
   use Base1,Base2 {  
     Base1::hola insteadof Base2;  
     Base2::adios insteadof Base1;  
     Base1::adios as alternativo;  
   }  
 }  
 $e = new Ejemplo2();  
 echo $e->hola('Ivan') . "<br/>";  
 echo $e->adios('Ivan') . "<br/>";  
 echo $e->alternativo('Ivan') . "<br/>";  

Como resultado obtendremos:

 Hola1: Ivan  
 Adios2: Ivan  
 Adios1: Ivan  


2. Cambio de visibilidad

Como hemos mencionado en el primer apartado, mediante el operador as podemos utilizar un alias en uno de los método que provoca conflicto de nombres y de esta forma poder utilizarlo. O también aplicar un alias sobre un método de nombre no conflictivo. Pero hay que saber que dicho operador tiene otra función además de la mencionada. Con el podemos cambiar la visibilidad del método de un 'trait'. O aplicar un alias y cambiar la visibilidad a la vez.

 trait :: nombre_método as nueva_visibilidad alias_método  

Si el alias lo aplicamos sobre un método que no ocasiona conflicto de nombre no hace falta especificar el 'trait' donde está definido.

 trait Base1 {  
   public function hola($nombre) {  
     return "Hola1: {$nombre}";  
   }  
   public function adios($nombre){  
     return "Adios1: {$nombre}";  
   }  
   private function prueba(){  
     return "Esto es una prueba";  
   }  
 }  
 trait Base2 {  
   public function hola($nombre) {  
     return "Hola2: {$nombre}";  
   }  
   public function adios($nombre){  
     return "Adios2: {$nombre}";  
   }  
   private function nombreHorrible(){  
     return "Voy a tener que cambiar el nombre";  
   }  
 }  
 class Ejemplo2 {  
   use Base1,Base2 {  
     Base1::hola insteadof Base2;  
     Base2::adios insteadof Base1;  
     Base1::adios as alternativo;  
     prueba as public;  //solo cambiamos visibilidad
     nombreHorrible as public cambio;  // cambiamos visibilidad y aplicamos alias
   }  
 }  
 $e = new Ejemplo2();  
 echo $e->hola('Ivan') . "<br/>";  
 echo $e->adios('Ivan') . "<br/>";  
 echo $e->alternativo('Ivan') . "<br/>";  
 echo $e->prueba() . "<br/>";  
 echo $e->cambio() . "<br/>"  

3. 'Traits' compuestos de otros 'traits'

Un 'trait' puede hacer uso de otros 'traits' en su definición.

 trait Trait1{  
   function hola($nombre){  
     return "Hola: " . $nombre;  
   }  
 }  
 trait Trait2{  
   function adios($nombre){  
     return "Adios: " . $nombre;  
   }  
 }  
 trait Trait3{  
   use Trait1,Trait2;  
 }  
 class ClaseEjemplo{  
   use Trait3;  
 }  
 $c = new ClaseEjemplo();  
 echo $c->adios('José');  
 echo $c->hola('José');  

4. 'Traits' y los métodos abstractos y estáticos

Un 'trait' puede contener métodos abstractos para obligar a la clase que lo utilice a implementar la funcionalidad de dicho métodos.

 trait Trait1{  
   function hola($nombre){  
     return "Hola: " . $nombre;  
   }  
   public abstract function prueba();  
 }  
 trait Trait2{  
   function adios($nombre){  
     return "Adios: " . $nombre;  
   }  
 }  
 trait Trait3{  
   use Trait1,Trait2;  
 }  
 class ClaseEjemplo{  
   use Trait3;  
   public function prueba() {  
     return "Esto es una prueba";  
   }    
 }  
 $c = new ClaseEjemplo();  
 echo $c->prueba();  

Además un 'trait' puede definir método estáticos para poder utilizarlos sin tener que crear una instancia de la clase que utiliza el 'trait'

 trait Trait1{  
   function hola($nombre){  
     return "Hola: " . $nombre;  
   }  
   public abstract function prueba();  
 }  
 trait Trait2{  
   function adios($nombre){  
     return "Adios: " . $nombre;  
   }  
   public static function suma($num1,$num2){  
     return $num1 + $num2;  
   }  
 }  
 trait Trait3{  
   use Trait1,Trait2;  
 }  
 class ClaseEjemplo{  
   use Trait3;  
   public function prueba() {  
     return "Esto es una prueba";  
   }    
 }  
 $c = new ClaseEjemplo();  
 echo $c->prueba() . "<br/>";  
 echo ClaseEjemplo::suma(4,5);  


Entradas relacionadas

PHP orientado a objetos - Introducción
PHP orientado a objetos - Herencia
PHP orientado a objetos - Clases abstractas e interfaces

2 comentarios:

  1. Interesante. Gracias. Pienso que es conveniente usar trait que interface

    ResponderEliminar
  2. Una clase puede ser herencia de una clase abstracta y a la vez implementar traits

    ResponderEliminar