- ¿Qué es la herencia?
- ¿Qué limitaciones tiene la herencia?
- ¿Cuándo NO deberíamos usar herencia?
- ¿Qué podemos hacer en su lugar?
- ¿Entonces cuándo usar herencia?
Ya tenemos una buena base en programación orientada a objetos (OOP), con conceptos como clases, objetos y métodos. Ahora vamos a profundizar en uno de los aspectos más complejos y a menudo mal entendidos: la herencia.
Hablaremos de cuándo realmente es útil, sus limitaciones y qué otras alternativas existen, todo de manera simple y directa, con ejemplos claros para facilitar la comprensión.
¿Qué es la herencia?
En teoría, suena muy útil. Pero en la práctica, puede volverse un problema si no se usa con cuidado.
Imagina que tenemos esta jerarquía:
class Usuario:
def __init__(self, nombre):
self.nombre = nombre
def saludar(self):
return f"Hola, soy {self.nombre}"
class Administrador(Usuario):
def borrar_usuario(self):
print("Usuario borrado")
Hasta acá todo bien. Pero si empezamos a juntar diferentes características (rol, origen de datos, país, gustos personales), todo comienza a complicarse.
¿Qué limitaciones tiene la herencia?
Imaginemos que trabajamos en un ecommerce y tenemos un concepto de Usuario. ¿Cómo lo clasificamos?
- Según el género:
UsuarioHombre,UsuarioMujer - Según el tipo de autenticación:
Usuario,Invitado - Según su rol:
Admin,Miembro - Según su cargo:
Programador,Tester,Gerente - Según la fuente:
UsuarioFacebook,UsuarioGitHub - Según el país:
UsuarioColombia,UsuarioMéxico
Si intentamos modelar todo eso con herencia, terminaríamos con docenas o incluso cientos de clases diferentes:
AdministradorDeFacebookQueViveEnColombiaYEsTester
Como podemos ver, esto se vuelve inmanejable. La herencia obliga a elegir una sola estructura jerárquica, cuando muchas veces necesitamos variar eso según el contexto.
Problemas comunes de la herencia
- Acoplamiento rígido: las clases quedan demasiado conectadas.
- Poca flexibilidad: cambiar la estructura es difícil.
- Confusión entre abstracciones: se mezclan la lógica de negocio con los detalles técnicos.
¿Cuándo NO deberíamos usar herencia?
Pensemos en esto:
class Usuario:
pass
class UsuarioSQL(Usuario):
pass
¿Tiene sentido que el tipo de almacenamiento (SQL) defina una nueva subclase de Usuario? En realidad, el Usuario como entidad no debería “saber” ni depender de cómo se guarda.
Este tipo de herencia mezcla niveles de abstracción: lo conceptual (usuario) con lo técnico (base de datos). Y eso tiende a causar errores difíciles de mantener luego.
¿Qué podemos hacer en su lugar?
Aquí entran dos conceptos fundamentales que nos ayudan a resolver estos problemas: composición y mixins.
1. Composición: Delegar tareas, no heredar
La composición es cuando una clase incluye a otra como parte de su funcionamiento, en lugar de heredarla. Nos da más flexibilidad.
Por ejemplo:
class AlmacenamientoSQL:
def guardar(self, datos):
print("Guardando en SQL:", datos)
class Usuario:
def __init__(self, nombre, almacenamiento):
self.nombre = nombre
self.almacenamiento = almacenamiento
def guardar(self):
self.almacenamiento.guardar({'nombre': self.nombre})
# Uso
sql = AlmacenamientoSQL()
usuario = Usuario("Juan", sql)
usuario.guardar()
Aquí no heredamos nada del almacenamiento; simplemente lo "inyectamos". De esta forma podemos cambiarlo por otro tipo sin tocar la clase Usuario.
2. Mixins: Reutilización sin herencia compleja
Otra alternativa muy usada es el patrón Mixin. Un mixin es una clase que define funciones específicas, y que otras clases pueden usar a través de herencia múltiple para tener esas capacidades, sin formar una jerarquía concreta.
Ejemplo:
class PuedeImprimir:
def imprimir(self):
print("Soy una instancia de", type(self).__name__)
class Usuario:
def __init__(self, nombre):
self.nombre = nombre
class UsuarioConImpresion(Usuario, PuedeImprimir):
pass
u = UsuarioConImpresion("Sara")
u.imprimir() # Imprime "Soy una instancia de UsuarioConImpresion"
Este patrón evita crear árboles gigantes de herencia, al permitir compartir funcionalidades independientes entre varias clases.
¿Entonces cuándo usar herencia?
La herencia sí tiene su lugar. Es útil cuando:
- Varias clases comparten código y comportamiento de forma natural.
- La relación entre clases tiene sentido conceptual (por ejemplo, un
Perrosí es unAnimal).
Pero si estás mezclando cosas como "de dónde se obtuvo un usuario" o "cómo se almacena", es mejor componer o usar mixins.
Casos reales: Comparación rápida
| Escenario | Herencia Tradicional | Composición | Mixin |
|---|---|---|---|
| Usuario que guarda en SQL | ❌ Mal diseño | ✅ Bien | ✅ Útil si es comportamiento reutilizable |
| Usuario Administrador | ✅ Ok si comparte comportamiento base | ✅ También posible | ✅ Si compartimos funciones como “exportar datos” |
| Usuario con impresión de info | ❌ Heredar de clase base solo para imprimir no tiene sentido | ❌ Poco útil | ✅ Ideal para mixins reutilizables |
Resumen
- La herencia es poderosa pero también puede hacer nuestro código complejo y poco flexible si la usamos mal.
- El mundo no siempre sigue jerarquías fijas; evitar modelarlo así ciegamente.
- Composición y Mixins son alternativas que permiten crear sistemas más modulares y adaptables.
- Usa herencia cuando haya una verdadera relación conceptual de tipo (“es un”).
- Evita mezclar abstracciones diferentes (como lógica de negocio y almacenamiento) en una jerarquía de clases.
Para acceder completo a curso necesitas un plan básico
El plan básico te dará acceso completo a todos los cursos, ejercicios y lecciones de Códica, proyectos y acceso de por vida a la teoría de las lecciones completadas. La suscripción se puede cancelar en cualquier momento.