Composición sobre Herencia: Una Perspectiva de Kotlin
En la programación orientada a objetos, existen dos formas principales de reutilizar código y establecer relaciones entre clases: herencia y composición. Aunque ambos enfoques tienen su lugar, el principio de “composición sobre herencia” ha ganado una tracción significativa en el diseño de software moderno. Esta entrada de blog explora ambos enfoques, sus compensaciones, y por qué la composición es a menudo la opción preferida, con ejemplos en Kotlin.
Entendiendo la Herencia
La herencia es un mecanismo donde una clase (subclase) puede heredar propiedades y comportamientos de otra clase (superclase). Establece una relación “es-un” entre clases.
|
|
En este ejemplo, Dog
hereda de Animal
y puede usar sus métodos mientras también añade su propio comportamiento.
Ventajas de la Herencia:
- Reutilización de código - Las subclases heredan automáticamente métodos y propiedades
- Sobrescritura de métodos - Permite la personalización del comportamiento heredado
- Polimorfismo - Permite tratar objetos de diferentes subclases como objetos de la superclase
Desventajas de la Herencia:
- Acoplamiento fuerte - Los cambios en la superclase pueden romper las subclases
- Problema de la clase base frágil - Las modificaciones a la clase base pueden tener efectos inesperados
- Inflexibilidad - La jerarquía de herencia se fija en tiempo de compilación
- Limitada a herencia simple en muchos lenguajes (incluyendo Kotlin)
Entendiendo la Composición
La composición es un principio de diseño donde las clases logran comportamiento polimórfico y reutilización de código conteniendo instancias de otras clases en lugar de heredar de ellas. Establece una relación “tiene-un”.
|
|
En este ejemplo, en lugar de heredar comportamiento, las clases Dog
y Cat
componen su comportamiento conteniendo instancias de SoundBehavior
y EatingBehavior
.
Ventajas de la Composición:
- Flexibilidad - Los comportamientos pueden cambiarse en tiempo de ejecución
- Desacoplamiento - Las clases son menos dependientes entre sí
- No hay problema de clase base frágil - Los cambios en un componente no afectan a otros
- Múltiples comportamientos - Puede incorporar múltiples comportamientos sin herencia múltiple
El Problema del Diamante y Por Qué Importa
Uno de los problemas clásicos con la herencia es el “problema del diamante”, que ocurre en escenarios de herencia múltiple:
A
/ \
B C
\ /
D
Si tanto B como C sobrescriben un método de A, ¿qué versión debería heredar D?
Aunque Kotlin no soporta herencia múltiple de clases, sí soporta implementación múltiple de interfaces, lo que puede llevar a problemas similares:
|
|
La composición evita este problema por completo haciendo las relaciones explícitas:
|
|
Ejemplo del Mundo Real: Componentes de UI
Veamos un ejemplo más práctico que involucra componentes de UI:
Enfoque de Herencia:
|
|
Enfoque de Composición:
|
|
Con la composición, podemos mezclar y combinar comportamientos sin crear una jerarquía de herencia compleja. Incluso podemos cambiar comportamientos en tiempo de ejecución:
|
|
Cuándo Usar Herencia
A pesar de las ventajas de la composición, la herencia todavía tiene su lugar:
- Cuando hay una clara relación “es-un” que es poco probable que cambie
- Cuando quieres aprovechar el polimorfismo de manera directa
- Cuando la clase base es estable y es poco probable que cambie frecuentemente
- Para el diseño de frameworks donde los puntos de extensión están bien definidos
Por ejemplo, en la biblioteca estándar de Kotlin, ArrayList
hereda de AbstractList
, lo que tiene sentido porque un ArrayList es fundamentalmente una lista y esta relación no cambiará.
Mejores Prácticas
- Favorece la composición sobre la herencia como regla general
- Usa herencia cuando hay una verdadera relación “es-un” que sea estable
- Diseña para la composición creando interfaces y clases pequeñas y enfocadas
- Considera la delegación como un punto intermedio (Kotlin tiene soporte incorporado con la palabra clave
by
) - Evita jerarquías de herencia profundas ya que se vuelven difíciles de entender y mantener
- Programa hacia interfaces, no implementaciones para facilitar la composición
La característica de delegación de Kotlin proporciona una forma conveniente de implementar la composición:
|
|
Conclusión
Aunque la herencia es una característica poderosa de la programación orientada a objetos, la composición a menudo proporciona un enfoque más flexible y mantenible para la reutilización de código y las relaciones entre clases. Al entender las compensaciones entre estos enfoques, puedes tomar mejores decisiones de diseño en tus proyectos de Kotlin.
Recuerda que un buen diseño no se trata de seguir reglas dogmáticamente, sino de elegir la herramienta adecuada para el trabajo. En muchos casos, esa herramienta será la composición, pero todavía hay casos de uso válidos para la herencia. La clave es entender las implicaciones de tu elección y diseñar tu código para que sea lo más flexible y mantenible posible.