Javi Moreno
Composición sobre Herencia: El Principio de Diseño que Hace tu Código Más Flexible

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.