jueves, 18 de julio de 2024

¿Herencia? Composición FTW





 "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.
Es tentador hacer una clase padre que sea MoveableObject, de la cual heredarían Ally y Enemy, pero luego habría que implementar 2 veces la cinematica en tierra y 2 veces la cinematica en aire.

De manera similar, podríamos hacer que las clases bases fueran FlyingObject y GroundObject, pero nos encontraríamos otra vez con el mismo problema.

Incluso si fuera posible estructurar las clases de manera que formen un árbol, aún se correría el riesgo de que tras algún cambio de planes se necesiten añadir más clases y echen a perder el diseño.

Otro problema bastante fuerte de la herencia es al escribir pruebas unitarias, si tienes una clase C, que hereda de una clase B, que a su vez hereda de una clase A, en el momento de escribir una prueba unitaria para la clase C, va a ser necesario crear fakes y mocks para todas las dependencias de las clases A y B, pese a que el código que se está probando es el de la clase C.

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?

Quizá se esté preguntando ¿hay algún caso en que convenga no usar composición?, ¿tiene alguna desventaja?, pues he aquí lo que me viene a la mente:
  • 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).
Por último, creo que una pregunta obligatoria sería ¿hay algún caso en que sea preferible usar herencia?, fuera de que(desgraciadamente) muchas APIs exigen el uso de la herencia, sólo se me ocurren casos muy raros donde se quiera tomar ventaja de la evaluación de tipos del compilador en cuestión.

También algunos podrían señalar que la herencia requiere menos código que la composición, así que para casos pequeños podría resultar mas conveniente usar herencia. El problema es que en un proyecto grande las partes del proyecto que parecían que iban a ser pequeñas tienden a volverse más grandes de lo esperado. Quizá para proyectos pequeños podría resultar adecuada pero no me atrevo a recomendarla (le doy el beneficio de la duda, ya que la experiencia me ha enseñado que ser muy tajante no suele ser buena idea).

No hay comentarios.:

Publicar un comentario

De StackOverflow a ChatGPT: El Arte de Pegar sin Pensar

 Es curioso cómo apenas en Agosto del año pasado critiqué duramente la práctica de copiar código de páginas de internet para pegarlo dentro ...