domingo, 4 de agosto de 2013

PHP orientado a objetos - Herencia

Introducción

A grandes rasgos, el termino herencia significa que una o varias clases pueden derivar de una clase base o 'padre'. Una clase que hereda de otra se llama subclase. Esta hereda todas los atributos y métodos de la clase 'padre'. Y además añade sus propias características (atributos y métodos). Por eso se suele decir que una subclase extiende a la clase base.
El concepto de herencia es el mismo que en el mundo real: hijos heredan características de los padres. Por lo que una subclase tendrá a su disposición todas los atributos y métodos de la clase base. Pero esta última (clase base) no podrá utilizar ninguna de las características que extiendan las clases herederas.
Podríamos concluir la introducción con lo dicho en los anteriores párrafos respecto al total acceso a las características de la clase base por parte de las subclases. Pero estaríamos equivocados. Ya que no hemos tenido en cuenta el concepto de visibilidad explicado en el anterior tutorial. Ya que como explicamos, los lenguajes de programación orientados a objetos permiten indicar que atributos y métodos son internos de la clase base y cuáles son accesibles desde fuera de la clase. Y eso quiere decir que la clase heredera solo podrá tener acceso a los atributos y métodos que se hayan especificado con una visibilidad pública (public) o protegida (protected). Los miembros privados  no serán accesibles directamente. Por lo que nuevamente estamos ante el concepto de encapsulación ya que  la clase hija no puede acceder a todas la implementación interna de la clase padre. Solamente puede acceder a ciertos atributos y métodos que la clase padre permita.

La regla general para la creación de subclase es que solamente se debe aplicar herencia cuando exista una relación “es un” entre clases. Esto quiere decir que solo se aplica herencia cuando la subclase va a tener en común todos los atributos y métodos de la clase base. Y no solo algunos. Un error común es querer aplicar herencia porque hay clases que tienen algunos atributos y métodos en común. Esta situación influye negativamente en el diseño de clases  ya que se estaría generalizando clases solamente para reutilizar código y no porque tengan una relación conceptual padre-hijo.

Principales ventajas

1. Si se aplica bien el concepto de herencia conseguiremos una reutilización eficiente del código de la clase base. Ya que estemos utilizando el mismo código en las dos clases, padre e hijo, pero solo lo habremos escrito una vez.

2. Una de las principales ventajas de la herencia es la propagación de los cambios realizados en la clase base. Cualquier cambio que se realice en las clases base sera accesible inmediatamente en las clases heredadas. Siempre cuando la visibilidad introducida lo permita. Sin embargo hay que tener cuidado con esta característica. Ya que cambios realizados en las clases base pueden causar problemas en las clases heredadas que han sido creadas por otras personas. Por lo que hay que asegurarse que los cambios son compatibles con las clases heredadas. Por ejemplo, podríamos añadir nuevos atributos y métodos y no modificar los existentes.

3. La herencia es transitiva. Esto significa que una clase puede heredar características de la clase base que se encuentra  niveles más arriba en la jerarquía de herencia. Por ejemplo si la clase Mercedes es una subclase de la clase Coche, y la clase Coche es una subclase de la clase Vehículo, entonces el Mercedes heredará atributos tanto de Coche como de Vehículo.

Herencia en PHP

PHP únicamente soporta herencia simple. Aunque es posible simular la herencia múltiple (una clase hereda de varias clases) mediante el uso de interfaces.
La sintaxis para la herencia simple en PHP se realiza mediante la palabra reservada extends:

 Hijo extends Padre{  
 }  

A continuación vamos a ver un ejemplo sencillo de herencia en PHP

1. Primero crearemos la clase base.

 class Producto {  
   protected $id;   
   private $titulo;  
   private $precio;  
   private $nombreAutor;  
   private $apellidosAutor;  
   function __construct($id, $titulo, $precio, $nombreAutor, $apellidosAutor) {  
     $this->id = $id;  
     $this->titulo= $titulo;  
     $this->nombreAutor = $nombreAutor;  
     $this->apellidosAutor = $apellidosAutor;  
     $this->precio = $precio;  
   }  
   public function getAutor() {  
     return $this->nombreAutor . " " . $this->apellidosAutor;  
   }  
   public function getTitulo(){  
     return $this->precio;  
   }  
   public function getPrecio(){  
     return $this->precio;  
   }  
 }  

Como vemos, hemos declarado varios atributos privados por lo que la única manera e acceder a ellas desde las clases herederas es mediante los métodos.

2. Clase que heredará de la clase base.

 class Libro extends Producto {    
   public function getResumen() {  
     $resumen = "Titulo: " . $this->getTitulo() . ", Precio: " . $this->getPrecio();  
     $resumen .= ", Autor: " .$this->getAutor() . ", Núm. páginas: " . $this->getNumPaginas();    
     return $resumen;  
   }  
 }  

Como no hemos definido ninguno constructor específico en esta clase, se heredará el de la clase base. Por lo tanto cuando creamos un objeto instanciado de la clase Libro deberemos de añadir los parámetros que necesitaba el constructor de la clase base.

 $libro1 = new Libro(1,"título",20,"Autor", "Apellido1 Apellido2");  
 echo $libro1->getResumen();  

Herencia y constructores

Cuando defines un constructor propio en la clase hija es necesario pasar los argumentos al constructor de la clase base. Por lo tanto, lo primero, para poder pasar dichos argumentos, es encontrar la manera de acceder al método la clase base. Y eso se hace usando la siguiente sintaxis:

 parent::__construct();  

De esta forma invocaremos al constructor de la clase base de la cual que hereda la clase actual.

Y ¿para que queremos definir un constructor (en la clase hija), y por lo tanto, invocar al constructor de la clase base? Pues muy sencillo, no siempre queremos que algún método de la clase hija se comporte exactamente igual que de la clase base. Seguramente querremos utilizar la funcionalidad del constructor padre y extenderla. Pero manteniendo la funcionalidad que aportaba el método heredado.
Imagina el ejemplo anterior en el que hemos definido la clase Producto y la clase Libro. La clase Libro no tiene definido un constructor, con lo que asume el de la clase base. Pero imagina que queremos añadir a la clase el atributo $numPaginas. Ahora necesitaremos un constructor propio ya que queremos incorporar el número de páginas en la inicialización de un libro.

 class Libro extends Producto {    
   private $numPaginas;  
   function __construct($id, $titulo, $precio, $nombreAutor, $apellidosAutor,$numPaginas) {  
     parent::__construct($id, $titulo, $precio, $nombreAutor, $apellidosAutor);  
     $this->numPaginas = $numPaginas;  
   }  
   public function getNumPaginas() {  
     return $this->numPaginas;  
   }  
   public function getResumen() {  
     $resumen = "Titulo: " . $this->getTitulo() . ", Precio: " . $this->getPrecio();  
     $resumen .= ", Autor: " .$this->getAutor() . ", Núm. páginas: " . $this->getNumPaginas();    
     return $resumen;  
   }  
 }  

Por lo tanto hemos creado un constructor que necesita los argumentos de la clase base y el nuevo argumento ($numPaginas). Y como nuestro constructor es un añadido al constructor base, hemos llamado a este con los argumentos necesarios y posteriormente hemos añadido la nueva funcionalidad.

 $libro1 = new Libro(1,"título",20,"Autor", "Apellido1 Apellido2",440);  
 echo $libro1->getResumen();  


Herencia y destructores

Como pasa con los constructores, los destructores de la clase base no serán llamados implícitamente por PHP. Así que si queremos ejecutar un destructor de la clase padre, se deberá llamar explícitamente a parent::__destruct() en el interior del destructor definido (sobreescrito).
Y como pasa con los constructores, si las clases hijas no implementan un destructor, se heredará y utilizará el de la clase base.

Sobreescritura

Al mecanismo de añadir funcionalidad a un método heredado, y por lo tanto llamado de la misma forma en ambas clase, se llama sobreescritura. Ya que estamos sobreescribiendo el funcionamiento del mismo. Y este mecanismo no solo se puede utilizar con los constructores sino que en cualquier método heredado de una clase base.
Lo más conveniente es sobreescribir métodos para completar el algoritmo del método de la clase base. Y no es recomendable sobreescribir un método y cambiar completamente su comportamiento. Ya que para eso lo mejor es crear un nuevo método en la clase hija.
Y al igual que con el constructor del apartado anterior, para invocar al método de la clase padre utilizaremos la sintaxis: parent::metodo($argumentos).

Siguiendo el ejemplo de Producto y Libro, podríamos definir en la clase base el método getResumen() y sobreescribir la función en la clase Libro para añadir funcionalidad:

 class Producto {  
    //...  
   protected function getResumen() {  
     $resumen = "Titulo: " . $this->getTitulo() . ", Precio: " . $this->getPrecio();  
     $resumen .= ", Autor: " . $this->getAutor();  
     return $resumen;  
   }  
 }  

 class Libro extends Producto {  
   //...  
   public function getResumen() {  
     $resumen = parent::getResumen();  
     $resumen .= ", Núm. páginas: " . $this->getNumPaginas();  
     return $resumen;  
   }  
 }  

Nota: Como no queremos que la función getResumen() pueda usarse fuera del contexto de Producto, se ha definido como protegida. Si embargo en Libro la sobreescritura de dicha función se ha definido como púbica para que desde fuera del objeto se pueda acceder a ella.

Visibilidad y herencia

- Los atributos y métodos declarados como públicos podrán utilizarse desde cualquier contexto: desde la misma clase, desde una clase heredada o desde el exterior.
- Los atributos y métodos declarados como privados no podrán ser accedidos desde otro sitio que no sea el interior de la clase en donde están declarados. Por lo tanto no se podrán sobreescribir características privadas en las clases heredadas. Ya que las clases hijas no tendrán acceso a esas características privadas.
- Los atributos y métodos declarados como protegidos solo podrán ser accedidos desde la clase que los declara o desde una clase heredera. Pero nunca desde el exterior de una clase.

Y como ya hemos comentado a lo largo de este tutorial y el anterior, gracias a la visibilidad solo se expondrán las características que sean requeridas por un cliente (que podría ser otra clase). Y evitar que se puedan modificar libremente atributos de un objeto. Ya que solo se podrán modificar los atributos que la definición de la clase permita y mediante los métodos requeridos para ello. Por lo tanto podríamos completar un poco las clases de los ejemplos anteriores para que cualquier modificación de un atributo se haga mediante métodos de acceso:

 class Producto {  
   protected $id;  
   private $titulo;  
   private $precio;  
   private $nombreAutor;  
   private $apellidosAutor;  
   function __construct($id, $titulo, $precio, $nombreAutor, $apellidosAutor) {  
     $this->id = $id;  
     $this->titulo = $titulo;  
     $this->nombreAutor = $nombreAutor;  
     $this->apellidosAutor = $apellidosAutor;  
     $this->precio = $precio;  
   }  
   public function getId(){  
     return $this->id;  
   }  
   public function getAutor() {  
     return $this->nombreAutor . " " . $this->apellidosAutor;  
   }  
   public function getTitulo() {  
     return $this->precio;  
   }  
   public function getPrecio() {  
     return $this->precio;  
   }  
   peotected function getResumen() {  
     $resumen = "Titulo: " . $this->getTitulo() . ", Precio: " . $this->getPrecio();  
     $resumen .= ", Autor: " . $this->getAutor();  
     return $resumen;  
   }  
   public function setId($id) {  
     $this->id = $id;  
   }  
   public function setTitulo($titulo) {  
     $this->titulo = $titulo;  
   }  
   public function setPrecio($precio) {  
     $this->precio = $precio;  
   }  
   public function setNombreAutor($nombreAutor) {  
     $this->nombreAutor = $nombreAutor;  
   }  
   public function setApellidosAutor($apellidosAutor) {  
     $this->apellidosAutor = $apellidosAutor;  
   }  
 }  

 class Libro extends Producto {  
   private $numPaginas;  
   function __construct($id, $titulo, $precio, $nombreAutor, $apellidosAutor, $numPaginas) {  
     parent::__construct($id, $titulo, $precio, $nombreAutor, $apellidosAutor);  
     $this->numPaginas = $numPaginas;  
   }  
   public function getNumPaginas() {  
     return $this->numPaginas;  
   }  
   public function setNumPaginas($numPaginas){  
     return $this->numPaginas = $numPaginas;  
   }  
   public function getResumen() {  
     $resumen = parent::getResumen();  
     $resumen .= ", Núm. páginas: " . $this->getNumPaginas();  
     return $resumen;  
   }  
 }  


Entradas relacionadas

PHP orientado a objetos - Introducción

3 comentarios:

  1. Debe corregirse:
    public function getTitulo() {
    return $this->precio;
    }
    datos correctos
    public function getTitulo() {
    return $this->Titulo;
    }

    ResponderEliminar
  2. Buen artículo bro, aunque demasiado técnico para un lego.

    ResponderEliminar
  3. Duda: ¿Qué no los métodos que se extienden de otra clase deben tener el mismo modificador de acceso? en getResumen() del padre (Producto) está protected, entonces en el hijo (Libro) también debe ser protected.
    Si lo que ocupas es que todos tengan un método getResumen() que se tenga que implementar de diferente forma en cada objeto, entonces Producto debe ser una clase abstracta y getResumen() debe ser un método abstracto.

    ResponderEliminar