Composición sobre Herencia: El Principio de Diseño que Hace tu Código Más Flexible
En el desarrollo de software, es común enfrentarse a la decisión entre usar herencia o composición. Aunque la herencia puede parecer la solución más sencilla al inicio, rápidamente puede generar jerarquías rígidas y difíciles de mantener. La composición, en cambio, ofrece una forma más flexible y escalable de diseñar tu aplicación.
Este artículo se inspira en la lectura “Composition over Inheritance: A Flexible Design Principle” y adapta los ejemplos al dominio de usuarios y permisos en aplicaciones web, con todo el código en JavaScript.
🚨 El problema de la herencia
Imagina que estás construyendo un sistema de gestión de usuarios. Podrías empezar con una clase base Usuario y extenderla para distintos tipos:
class Usuario {
constructor(nombre) {
this.nombre = nombre;
}
login() {
console.log(`${this.nombre} ha iniciado sesión`);
}
}
class UsuarioAdmin extends Usuario {
borrarUsuario(user) {
console.log(`${this.nombre} eliminó al usuario ${user.nombre}`);
}
}
class UsuarioEditor extends Usuario {
editarPost(postId) {
console.log(`${this.nombre} editó el post ${postId}`);
}
}
const admin = new UsuarioAdmin("Ana");
admin.login();
admin.borrarUsuario({ nombre: "Carlos" });
const editor = new UsuarioEditor("Luis");
editor.login();
editor.editarPost(42);
🤔 ¿Qué problema hay aquí?
- Cada vez que añadimos un nuevo tipo de usuario con capacidades distintas, debemos crear una nueva subclase.
- Si un usuario necesita una combinación de habilidades (por ejemplo, editar y borrar), tenemos que crear otra clase o duplicar lógica.
- La jerarquía crece y se vuelve difícil de mantener.
✅ La solución: composición
Con composición, en lugar de extender clases, combinamos pequeños comportamientos reutilizables para formar objetos más complejos.
// Comportamientos independientes
const puedeLoguear = (usuario) => ({
login: () => console.log(`${usuario.nombre} ha iniciado sesión`)
});
const puedeBorrar = (usuario) => ({
borrarUsuario: (user) => console.log(`${usuario.nombre} eliminó al usuario ${user.nombre}`)
});
const puedeEditar = (usuario) => ({
editarPost: (postId) => console.log(`${usuario.nombre} editó el post ${postId}`)
});
// Factoría de usuarios
const crearUsuario = (nombre, ...habilidades) => {
const usuario = { nombre };
return Object.assign(usuario, ...habilidades.map((fn) => fn(usuario)));
};
// Crear instancias con distintas combinaciones
const admin = crearUsuario("Ana", puedeLoguear, puedeBorrar);
const editor = crearUsuario("Luis", puedeLoguear, puedeEditar);
const superUsuario = crearUsuario("Marta", puedeLoguear, puedeBorrar, puedeEditar);
// Uso
admin.login();
admin.borrarUsuario({ nombre: "Carlos" });
editor.login();
editor.editarPost(42);
superUsuario.login();
superUsuario.borrarUsuario({ nombre: "Luis" });
superUsuario.editarPost(101);
🎉 Ventajas
- Podemos mezclar habilidades libremente sin necesidad de crear una nueva subclase.
- El código es más modular y reutilizable.
- Las responsabilidades están mejor separadas.
- Es más sencillo probar cada comportamiento de forma aislada.
🧩 Composición con funciones puras y cierres (closures)
La composición se integra muy bien con funciones puras y cierres, evitando estado compartido innecesario.
// Permisos con estado interno encapsulado
const conPermisos = (permisosIniciales = []) => (usuario) => {
let permisos = new Set(permisosIniciales);
return {
agregarPermiso: (permiso) => permisos.add(permiso),
quitarPermiso: (permiso) => permisos.delete(permiso),
tienePermiso: (permiso) => permisos.has(permiso),
listarPermisos: () => Array.from(permisos),
};
};
const con2FA = (usuario) => {
let activo = false;
return {
activar2FA: () => (activo = true),
desactivar2FA: () => (activo = false),
is2FAActivo: () => activo,
};
};
const crearUsuarioSeguro = (nombre, ...mixins) => {
const base = { nombre };
return Object.assign(base, ...mixins.map((m) => m(base)));
};
const usuario = crearUsuarioSeguro(
"Sofía",
conPermisos(["editar"]),
con2FA
);
usuario.agregarPermiso("borrar");
usuario.activar2FA();
console.log(usuario.listarPermisos()); // ["editar", "borrar"]
console.log(usuario.is2FAActivo()); // true
🔌 Componer sin acoplar: dependencia por inyección
La composición también facilita la inyección de dependencias (por ejemplo, un servicio de auditoría), manteniendo los módulos desacoplados.
const conAuditoria = (logFn) => (usuario) => ({
audit: (accion, payload = {}) => logFn({
usuario: usuario.nombre,
accion,
payload,
timestamp: new Date().toISOString()
}),
});
const consolaLogger = (evento) => console.log("[AUDIT]", evento);
const conModeracion = (usuario, audit) => ({
banearUsuario: (target) => {
audit("BANEAR_USUARIO", { target });
console.log(`${usuario.nombre} baneó a ${target}`);
},
});
const crearModerador = (nombre, logger) => {
let audit;
const usuario = { nombre };
const auditoria = conAuditoria(logger)(usuario);
audit = auditoria.audit;
return Object.assign(usuario, auditoria, conModeracion(usuario, audit));
};
const mod = crearModerador("Julia", consolaLogger);
mod.banearUsuario("carlos123");
// [AUDIT] { usuario: 'Julia', accion: 'BANEAR_USUARIO', payload: { target: 'carlos123' }, timestamp: '...' }
⚖️ ¿Cuándo heredar y cuándo componer?
Usa herencia cuando exista una relación clara “es-un” y el comportamiento sea estable a largo plazo.
Prefiere composición cuando:
- necesites combinaciones flexibles de capacidades;
- quieras evitar jerarquías profundas;
- busques reutilización y bajo acoplamiento;
- preveas cambios frecuentes en requisitos.
Regla práctica: si tu jerarquía crece en “diamantes” o en “multiplicación” de subclases, cambia a composición.
🧪 Testeabilidad mejorada
La composición permite probar cada mixin o comportamiento por separado:
// Ejemplo simple de prueba conceptual (sin framework)
const registros = [];
const testLogger = (e) => registros.push(e);
const modTest = crearModerador("Test", testLogger);
modTest.banearUsuario("usuarioX");
console.assert(registros.length === 1, "Debe registrar 1 evento");
console.assert(registros[0].accion === "BANEAR_USUARIO", "Acción incorrecta");
📚 Fuente y recursos
- MDN — Object Composition.
- Clean Code JavaScript — Patrones de composición.