¿Qué es el patrón estructural Composite?
El patrón Composite (compuesto) te ayuda a modelar estructuras “en árbol”: elementos que pueden contener otros elementos, como un menú con submenús, o una carpeta con archivos y subcarpetas. Su idea central es simple: tratar un elemento individual y un grupo de elementos de la misma manera.
En vez de llenar tu código de condiciones del tipo “si es carpeta, recorre; si es archivo, calcula”, Composite propone una interfaz común (por ejemplo, calcularTamanio() o mostrar()), y tanto los elementos “hoja” (los individuales) como los “compuestos” (los que contienen otros) implementan esa misma interfaz.
¿Qué problema resuelve (en palabras simples)?
Cuando estás empezando, es normal escribir código con muchos if para diferenciar tipos de objetos. Eso funciona al principio, pero se vuelve difícil de mantener cuando:
- Aparecen nuevos tipos de nodos.
- Necesitás repetir operaciones: imprimir, calcular totales, validar, exportar.
- La estructura crece en profundidad (subniveles).
Con Composite, tu código cliente (la parte que usa los objetos) se vuelve más limpio: llama a un método y cada objeto sabe qué hacer, ya sea un elemento simple o un conjunto.
Piezas principales del patrón
- Componente: la interfaz común. Define operaciones como
render(),getPrice(),print(), etc. - Hoja (Leaf): un elemento individual que no contiene hijos (por ejemplo, un
Archivo, unItemde menú sin submenú). - Compuesto (Composite): un contenedor que tiene hijos y que implementa la misma interfaz (por ejemplo,
Carpeta,Submenu). - Cliente: el código que usa la estructura sin preocuparse por si es hoja o compuesto.
Caso de uso 1 (TypeScript): Menú de navegación con submenús
Imaginá que estás construyendo un panel de administración. El menú puede tener enlaces simples (por ejemplo, “Usuarios”) y secciones que agrupan opciones (por ejemplo, “Reportes” con subopciones). Querés poder renderizar todo el menú con una sola llamada.
type RenderableMenu = {
render(indent?: number): string;
};
// Hoja: un link simple
class MenuItem implements RenderableMenu {
constructor(private label: string, private href: string) {}
render(indent: number = 0): string {
const pad = " ".repeat(indent);
return `${pad}- ${this.label} (${this.href})`;
}
}
// Compuesto: una sección que contiene otros items (hojas o compuestos)
class MenuSection implements RenderableMenu {
private children: RenderableMenu[] = [];
constructor(private title: string) {}
add(child: RenderableMenu): this {
this.children.push(child);
return this;
}
render(indent: number = 0): string {
const pad = " ".repeat(indent);
const header = `${pad}* ${this.title}`;
const body = this.children.map(c => c.render(indent + 2)).join("\n");
return body ? `${header}\n${body}` : header;
}
}
// --- Cliente ---
const rootMenu = new MenuSection("Panel");
rootMenu
.add(new MenuItem("Dashboard", "/dashboard"))
.add(
new MenuSection("Reportes")
.add(new MenuItem("Ventas", "/reportes/ventas"))
.add(new MenuItem("Inventario", "/reportes/inventario"))
)
.add(new MenuItem("Usuarios", "/usuarios"));
console.log(rootMenu.render());
/* Salida:
* Panel
- Dashboard (/dashboard)
* Reportes
- Ventas (/reportes/ventas)
- Inventario (/reportes/inventario)
- Usuarios (/usuarios)
*/Lo importante es que MenuItem y MenuSection comparten el método render(). El cliente solo llama rootMenu.render() y listo: se renderiza todo el árbol.
Caso de uso 2 (Java): Sistema de archivos (carpetas y archivos)
Otro ejemplo clásico y muy realista: calcular el tamaño total ocupado por una carpeta. Una carpeta contiene archivos y subcarpetas, y cada subcarpeta puede contener más cosas. Con Composite, el cálculo queda natural: cada nodo responde sizeInBytes().
import java.util.ArrayList;
import java.util.List;
interface Node {
String name();
long sizeInBytes();
}
// Hoja: archivo
class FileNode implements Node {
private final String name;
private final long size;
public FileNode(String name, long sizeInBytes) {
this.name = name;
this.size = sizeInBytes;
}
public String name() {
return name;
}
public long sizeInBytes() {
return size;
}
}
// Compuesto: carpeta
class FolderNode implements Node {
private final String name;
private final List<Node> children = new ArrayList<>();
public FolderNode(String name) {
this.name = name;
}
public FolderNode add(Node child) {
children.add(child);
return this;
}
public String name() {
return name;
}
public long sizeInBytes() {
long total = 0;
for (Node child : children) {
total += child.sizeInBytes();
}
return total;
}
}
public class Main {
public static void main(String[] args) {
FolderNode project = new FolderNode("project");
project
.add(new FileNode("README.md", 1200))
.add(new FileNode("logo.png", 250_000))
.add(
new FolderNode("src")
.add(new FileNode("Main.java", 3400))
.add(new FileNode("Utils.java", 2100))
);
System.out.println("Total bytes: " + project.sizeInBytes());
}
}De nuevo, el cliente trata igual a FileNode y FolderNode porque ambos implementan Node. La carpeta solo suma lo que devuelven sus hijos, sin lógica especial en el cliente.
Cómo implementarlo paso a paso (rápido y sin vueltas)
- Definí una interfaz con la operación que te interesa (por ejemplo,
render(),total(),sizeInBytes()). - Creá una clase hoja (Leaf) que implemente la interfaz con lógica simple.
- Creá una clase compuesta (Composite) que también implemente la interfaz, pero delegue la operación a sus hijos (por ejemplo, recorriéndolos).
- Agregá métodos
add/removesolo donde tenga sentido (normalmente en el compuesto). - Usá el árbol desde el cliente llamando a la interfaz, sin preguntar “¿qué tipo es?”.
Preguntas frecuentes (con respuestas)
¿Composite reemplaza a los if/else cuando hay jerarquías?
En muchos casos, sí. Composite ayuda a reducir condicionales del tipo “si es carpeta…” o “si es menú…” porque define una operación común (por ejemplo, render() o sizeInBytes()) y deja que cada objeto la implemente. Así, el código cliente llama al mismo método sin preguntar qué tipo exacto es cada nodo. Aun así, no es magia: si tu estructura no es realmente jerárquica, un if/else simple puede ser más claro.
¿Cuándo conviene usarlo de verdad?
Cuando tu problema se parece a una estructura con niveles: menús, categorías de productos, comentarios con respuestas, carpetas y archivos, organigramas, escenas de un juego, etc. Si no hay jerarquía o no necesitás tratar “grupo e individual” igual, probablemente sea innecesario.
¿Qué error común debería evitar si estoy empezando?
Evitar meter demasiadas operaciones en la interfaz desde el inicio. Si tu interfaz tiene muchos métodos, las hojas terminan implementando cosas que no aplican (por ejemplo, add() en un archivo). Empezá con 1 o 2 operaciones y crecé con cuidado.
Ventajas y desventajas (en simple)
| Aspecto | Qué te aporta |
|---|---|
| Código cliente más simple | Llamás el mismo método sin diferenciar si es hoja o grupo. |
| Escalabilidad en jerarquías | Agregar subniveles no rompe la idea, el árbol crece naturalmente. |
| Extensión de la estructura | Podés sumar nuevos tipos de nodos si respetan la interfaz. |
| Riesgo de sobre-diseño | Si tu estructura es plana, Composite puede ser demasiado. |
| Interfaces muy grandes | Si abusás, obligás a implementar métodos innecesarios. |
Si estás aprendiendo patrones de diseño, Composite es uno de los más útiles porque aparece en problemas cotidianos. Buscá en tus proyectos cualquier estructura con “padre/hijo” (menús, categorías, carpetas) y probá reemplazar condicionales por una interfaz común. Vas a notar cómo tu código se vuelve más fácil de leer y de extender.