"Preferir Composición sobre Herencia" es uno de los lemas más comunes cuando se habla de patrones de diseño.
Desafortunadamente, en mi pequeña investigación que hice sobre los artículos en español que hablan de este tema encontré que todos tenían errores y ninguno hablaba de la ventaja de reducir la cantidad de dependencias. Así que tal vez yo sí pueda aportar algo con esto después de todo.
Voy a asumir que quien sea que lea esto ya sabe lo que es la herencia; y pese a ser una piedra angular de la programación orientada a objetos, presenta muchos problemas.
Problemas con la Herencia
En primer lugar la herencia múltiple presenta el problema del diamante, no me quiero detener a explicarlo porque es un problema bastante conocido.
¿...y que tal la herencia simple?, pues aquí sucede que no siempre es fácil estructurar las clases siguiendo un árbol de herencia simple. Aquí pongo un ejemplo muy sencillo, imagina que estamos haciendo un videojuego(RTS) y va a tener las siguientes clases:
- GroundAlly puede caminar, disparar, refugiarse en una construcción aliada y además es controlada por indicaciones del jugador
- FlyingAlly puede volar, disparar, refugiarse en una construcción aliada y es controlado por indicaciones del jugador.
- GroundEnemy puede caminar, disparar y es controlado por una IA.
- FlyingEnemy puede volar, disparar y es controlado por una IA.
Hay una conferencia que me gustó mucho llamada "Inheritance is the Base Class of Evil", y les recomiendo verla, ya que muestra bastantes otros problemas que los aquí mencionados:
https://www.youtube.com/watch?v=bIhUE5uUFOA
... pero yo me voy a centrar en resolver lo que mencioné aquí.
Composición
Primero que nada hay que mencionar qué es la composición.
A groso modo se trata de que un objeto almacene en su interior una instancia de una interfaz y la mande llamar:
export interface IMoveableObject {
getPosition() : {x : number, y : number};
setVelocity(x : number, y : number) : void;
}
export class ArriveSteering {
private target : {x : number, y : number};
max_speed = 100;
alpha = 10;
constructor(private entity : IMoveableObject, target : {x : number, y : number}) {
this.target = {...target};
}
update() : void {
const disp = sub(this.target, this.entity.getPosition());
const velocity = clamp(0, this.max_speed, mult(this.alpha, disp));
this.entity.setVelocity(velocity);
}
}
Esto provee una manera de lograr polimorfismo sin tener que extender clases (ya que se puede cambiar la implementación de getPosition y setVelocity y hacer que ArriveSteering acabe funcionando diferente).
Una forma de lograr el primer ejemplo que se mencionó(el del RTS) usando composición sería:
- Crear una interfaz llamda Behaviour, y haya 2 clases que implementen esta interfaz: una través de IA y la otra a través de órdenes del jugador.
- Crear otra interfaz llamada Action, la cual sería implementada en clases para disparar, caminar y volar
- Crear una clase llamada Entity, que reciba un arreglo de Action y un Behaviour.
Ventajas de la Composición sobre la Herencia
Las ventajas de la composición como sustituto de la herencia deberían estar claras:
- Menos dependencias (esto sirve de mucho en las pruebas unitarias)
- El árbol de tipos queda mucho más limpio.
¿Cuándo no usar composición?
- Es posible escribir una función que opere con un objeto que implementa una interfaz sin necesidad de crear una clase (esto NO es un caso de uso de la herencia sino para resaltar que a veces ni siquiera es necesario componer).
- Está la posibilidad de usar agregación(crear objetos dentro de la clase) e implementar interfaces sin extender (cuando no se extiende no sucede el problema del diamante y se pueden hacer cosas parecidas a la herencia múltiple).
En el ejemplo del RTS se podrían crear interfaces GroundObject, FlyingObject, AllyObject, EnemyObject, luego crear las 4 clases e implementar las interfaces correspondientes utilizando otras clases para reutilizar el código repetido. - A veces resulta un tanto raro que un montón de objetos diferentes sean de exactamente el mismo tipo (dado que su única diferencia es el contenido). Así que varias de estas veces los programadores acaban usando herencia simplemente para fijar los objetos que se componen (no sé si esto sea un anti-patrón).

No hay comentarios.:
Publicar un comentario