La forma en la que todos nos adentramos en el mundo de la programaciÛn orientada a objetos suele ser parecida. Tendemos a depender en exceso de la herencia, a realizar jerarquÌas de clases muy cerradas y muy estrictas, a solucionar cualquier problema, por pequeÒo que sea, a base de crear clases hijas particularizadas para ese problema. øPero es Èsa la mejor forma de organizar nuestro cÛdigo?
Hace poco, alguien me dijo que discutir sobre herencia y composiciÛn era como discutir sobre pc vs mac. Yo creo que no es asÌ. Voy a intentar explicar por quÈ
La herencia es un mecanismo muy poderoso, probablemente sea la herramienta m·s poderosa que nos ofrece cualquier lenguaje orientado a objetos, y probablemente, por eso tambiÈn, la que es m·s susceptible de ser mal utilizada. Pero primero veamos quÈ es la herencia.
B·sicamente, la herencia permite definir una clase utilizando como base otra clase ya existente. Ve·moslo con un ejemplo claro ( probablemente, el ejemplo m·s utilizado de la historia ).
class Persona
{
function Persona( )
{
}
public function comer( ){}
public function dormir( ){}
public function hablar( ){}
}
class Programador extends Persona
{
function Programador( ){}
}
Class Camionero extends Persona
{
function Camionero( ){}
}
Tanto el programador como el camionero ( en ambos casos, mientras no se demuestre lo contrario ) son personas, por lo que habr· una serie de comportamientos o acciones comunes a ambos ( comer, dormir, hablar ) que son las que les corresponden por el hecho de ser personas. Por tanto, aquÌ ya se nos han planteado los dos conceptos que van m·s unidos a la herencia.
Por un lado, la herencia permite la reutilizaciÛn de cÛdigo. Cuando haya varias clases que tengan que implementar una serie de mÈtodos comunes, y para evitar tener el mismo cÛdigo ( o cÛdigo muy parecido ) en muchos sitios, podemos simplemente implementar ese cÛdigo com˙n en una clase base, y hacer que el resto de clases hereden de Èsta.
Por otra parte, la herencia tambiÈn sirve para agrupar las clases conceptualmente. En este caso, tanto el programador como el camionero son personas, por lo tanto, es lÛgico que las clases que los representan hereden de la clase persona. De hecho, en todos los libros de programaciÛn orientada a objetos, Èsta es la norma que se da para saber cu·ndo se debe hacer que una clase herede de otra, la famosa "es-un". El programador "es una" persona, el camionero "es una" persona, por lo tanto deben heredar de la clase persona.
Adem·s, de esta manera, el polimorfismo es inmediato. Podemos utilizar una variable del tipo de la clase base para almacenar una instancia de cualquiera de sus subclases. En nuestro ejemplo, una variable de tipo Persona nos permitir· hacer referencia a un Programador, a un Camionero, etc.
La relaciÛn de composiciÛn, por el contrario, es una relaciÛn de pertenencia. Ve·moslo con un ejemplo.
class Programador extends Persona
{
var mascota: Mascota;
function Programador( )
{
this.mascota = new Mascota( );
}
}
El programador tiene una mascota. …sa es la palabra clave para definir una relaciÛn de composiciÛn ( "tiene un" ).
Bien, hasta ahora todo parece estar bastante claro. Entonces, øporquÈ el tÌtulo del post es "en caso de duda, composiciÛn"?. øEs que puede haber duda ?.
Pues sÌ, hay dudas, y muchas, sobre si la herencia es realmente tan beneficiosa como parece. Veamos algunos casos en los que las implementaciones basadas en la herencia no son la mejor soluciÛn de entre todas las posibles.
En primer lugar, vamos a replantearnos el primer ejemplo. Ahora, vamos a implementar una clase "Trabajador", que es la que va a tener todas las caracterÌsticas propias de una persona que tiene un trabajo, y que, obviamente, heredar· de la clase Persona ( un trabajador "es una" Persona ).
class Trabajador extends Persona
{
function Trabajador( ){}
public function entrarEnElTrabajo( ){}
public function trabajar( ){}
public function salirDelTrabajo( ){}
}
Por lo tanto, ahora, las clases Programador y Camionero serÌan:
class Programador extends Trabajador
{
}
class Camionero extends Trabajador
{
}
De este modo, tanto el Programador como el Camionero heredarÌan los mÈtodos de la clase Persona y los de la clase Trabajador.
Bien, tenemos una clase base ( Persona ) de la que extiende la clase Trabajador ( Un trabajador es una persona ), de la que a su vez extienden la clase Programador ( un Programador es un trabajador ) y Camionero ( que tambiÈn es un trabajador ). øCorrecto?. Bueno, pues no del todo. øPorquÈ?. Pues porque un programador no es sÛlo un trabajador. De hecho, algunos programadores, cuando salen de trabajar se convierten en novios, otros en amigos de alguien, otros en cinÈfilos, etc. Luego podemos decir que un programador no es siempre un trabajador.
Bien, pues como un programador no es siempre y en todo caso un trabajador, no deberÌa extender de la clase Trabajador, porque su comportamiento no va a ser siempre el de Trabajador. Dicho de otra forma, la prueba de "es un" para decidir si heredar, deberÌa refinarse m·s, y ser "es un y sÛlo un y siempre un".
En este caso, por ejemplo, la soluciÛn podrÌa pasar por que la funcionalidad correspondiente a los trabajadores formara parte de un interfaz que implementaran tanto el programador como el camionero. O, mejor a˙n, las clases Programador y Camionero pueden tener una variable de clase que sea una instancia de la clase Trabajador.
class Programador extends Persona
{
var workerPart: Trabajador = new Trabajador( );
public function entrarEnElTrabajo( )
{
this.workerPart.entrarEnElTrabajo( );
}
}
Esta soluciÛn nos trae otra ventaja. Si tuviÈramos que cambiar el perfil de alguno de los mÈtodos de la clase Trabajador, ese cambio sÛlo afectarÌa a las clases Programador y Camionero, ya que podrÌamos resolver ese cambio de perfil en ellas, por lo que el resto de clases que interact˙en con ellas no tienen porquÈ "notar" ese cambio. Hemos ganado tambiÈn en facilidad de refactorizaciÛn.
Pero no acaban ahÌ los problemas. Seguiremos en el prÛximo post.