PHP - Herencia básica

La herencia es otra de las características fundamentales de la programación orientada a objetos. El objetivo final de la herencia de clases es la reutilización de código.

Herencia básica

La idea básica de diseño es que primero se construyen las clases más genéricas (con el mayor grado de abstracción posible) y a partir de éstas se derivan las clases más especializadas.

Como la clase derivada hereda las características y la funcionalidad de la clase base, cada nuevo nivel en la jerarquía de clases implicará un grado más de especialización.

Por ejemplo, la clase Vehiculo de ejemplos anteriores contiene características y funcionalidad comunes a casi todos los vehículos. Si se necesita trabajar con un coche (un coche es un tipo especializado de vehículo) se puede derivar la clase Vehiculo para conseguir la clase Coche, y añadir a esta nueva clase las características específicas de los coches, así como la funcionalidad necesaria.


//// un coche 'es' un vehiculo
class Coche extends Vehiculo
{

  public $capacidadMaletero;
  public $numeroPuertas;

  //// constructor
  function __construct($potencia,$peso,$puertas=5)
  {
    $this->potencia = $potencia;
    $this->peso = $peso;
    $this->numeroPuertas = $puertas;

    return true;
  }

}

//// creamos un coche (60 CV, 1500 Kg)
$objCoche = new Coche(60,1500);

//// calculamos su relacion peso/potencia
echo $objCoche->relacionPesoPotencia();

Se puede ir más allá y modelar un Fórmula 1, que es un tipo especializado (muy especializado) de coche:

//// un formula 1 'es' un coche
class Formula1 extends Coche
{
  public $versionDeMotor;
  public $controlDeTraccionActiva;

  function __construct($potencia,$peso)
  {
    $this->potencia = $potencia;
    $this->peso = $peso;

    $this->numeroPuertas = 0;
    $this->capacidadMaletero = 0;

    return true;
  }
}

//// creamos un coche (900 CV, 600 Kg)
$objFerrari = new Formula1(900,600);

//// calculamos su relacion peso/potencia
echo $objFerrari->relacionPesoPotencia();

La funcionalidad de la clase Vehiculo es heredada por la clase Coche, la de Coche por Formula1… No es necesario volver a implementar dicha funcionalidad para cada nuevo tipo de objeto, simplemente la reutilizamos.

Está claro que cuanto mejor sea el diseño de una jerarquía de clases mayor será su rendimiento (en este contexto, rendimiento significa productividad a la hora de generar una solución que resuelva un problema de la forma más rápida, robusta y eficiente posible).

Métodos y atributos finales

En ocasiones es útil tener cierto control sobre la forma en que las clases derivadas sobrescriben ciertos métodos de la clase base (puesto que podrían cambiar la funcionalidad para la que fue diseñada inicialmente la clase). Por ejemplo, sería poco lógico que el método relacionPesoPotencia() de la clase Coche calcule en realidad el peso del coche en la Luna…¿?

Con el cualificador ‘final’, el diseñador de una clase puede obligar a que un determinado método no se pueda definir en las clases derivadas y sustituya (override) la funcionalidad que proporciona la clase base. Por ejemplo:

class Vehiculo
{
  //// $peso se declara como atributo final de la clase
  final $peso;

  protected $potencia;
  private $relacionPesoPotencia;

  function __construct($potencia,$peso)
  {
    $this->potencia = $potencia;
    $this->peso = $peso;

    return true;

  }

  //// se declara como metodo final de la clase
  final function relacionPesoPotencia()
  {
    if ($this->potencia>0)
    {
      $this->relacionPesoPotencia = ($this->peso/$this->potencia);
    }else
    {
      $this->relacionPesoPotencia = -1;
    }

    return $this->relacionPesoPotencia;
  }

}


class Moto extends Vehiculo
{
  public $cilindrada;
  public $tipoMotor;

  //// ERROR: se redefine el atributo $peso
  //// (declarado como final)
  public $peso;
  protected $potencia;
  private $relacionPesoPotencia;

  function __construct($potencia,$peso)
  {
    $this->potencia = $potencia;
    $this->peso = $peso;

    return true;

  } //// fin de "__constructor"

  //// ERROR: se redefine el metodo relacionPesoPotencia()
  //// (declarado como final)
  function relacionPesoPotencia()
  {
     return Vehiculo::relacionPesoPotencia();
  }

}

Herencia en proyectos reales

Sobre el papel la herencia parece una herramienta muy poderosa.

Permite reutilizar el código y permite crear una jerarquía de clases que comparten ciertas características.

El principal problema con la herencia es que nos obliga a prever el futuro: tenemos que saber de antemano todas las posibilidades y todas las variantes que pueden aparecer como clases hijas, nietas, etc.

Una vez establecida una estructura jerárquica, esa estructura estará fuertemente ligada.

Si más adelante aparece un caso de uso que no fue contemplado inicialmente, puede ocurrir que la estructura jerárquica actual ya no encaje bien con ese nuevo ‘objeto’.

Modificar la estructura jerárquica de clases puede llegar a ser muy complejo. Y es posible que acabemos haciendo algún ‘apaño’ para salvar la situación.

Intentar mapear el mundo real mediante una jerarquía de clases (herencia) lleva siempre a situaciones problemáticas.

La herencia forma estructuras de tipo árbol. El mundo real sigue relaciones mucho más complejas.

Un ejemplo típico podría ser:

Clase Animal para abstraer información sobre animales.

Hay animales que vuelan. Podemos abstraerlos en una clase AnimalVolador.

Hay animales que nadan. Podemos abstraerlos en una clase AnimalNadador.

Pero hay animales que vuelan y nadan… ¿dónde los colocamos?

Si se quieren tener en cuenta todas las posibles opciones a futuro probablemente acabaremos con un grado de abstracción enorme en la jerarquía.

En muchos casos el remedio (usando herencia) es peor que la enfermedad.

La herencia sí funciona muy bien y es muy útil para jerarquías pequeñas y muy básicas.

En la mayoría de las situaciones y casos de uso es preferible usar composición en lugar de herencia.

Siguiente artículo: control de acceso protected