🚨 ¡Nueva review! 🔇 Los mejores cascos con ANC del mercado: los Sony WH-1000XM4 . ¡Échale un ojo! 👀

El Patrón Composite: La Estructura de Cajas y Objetos

Trata a un objeto individual y a un grupo de objetos de la misma manera.

Escrito por domin el 28 de octubre de 2025 · Actualizado el 8 de febrero de 2026

📦 El Patrón Composite: Organiza la Jerarquía como un Profesional

El Patrón de diseño Composite es un patrón estructural que resuelve un problema muy común. ¿Cómo puedes tratar un objeto individual y un grupo de objetos de la misma forma, sin que tu código se vuelva una locura de condiciones if?

La respuesta está en que todos tengan un contrato igual para que todos se comporten de la misma manera.

Piensa en el explorador de archivos de tu ordenador. Cuando haces clic derecho en una carpeta y le das a “Propiedades”, te dice el tamaño total. Esa carpeta puede tener archivos sueltos, subcarpetas, subcarpetas dentro de subcarpetas… Da igual. Para ti es una sola operación: “dime cuánto pesas”. Eso es Composite.


📦 1. La caja de mudanza

Imagina que estás organizando una mudanza y necesitas calcular el peso total de todo lo que vas a mover.

Libro, Taza (Hoja)

Se pesan a sí mismos. Son el final de la cadena. No contienen nada dentro.

Caja (Compuesto)

Le pide el peso a cada "ElementoTransportable" que contiene y suma todo. Puede contener libros, tazas u otras cajas.

Al tratar al Libro y a la Caja como un ElementoTransportable, puedes sumar el peso de la casa entera con una sola llamada, sin importar cuántas cajas anidadas haya, esa es la magia.

¿Qué tipo de patrón de diseño es Composite?


🛠️ 2. Los roles del patrón

Componente (Interfaz)

La interfaz o clase abstracta común. Define las operaciones que todos deben implementar. En nuestro ejemplo: calcularPeso().

Hoja (Leaf)

Los objetos finales. No tienen hijos. Simplemente ejecutan la operación sobre sí mismos: devuelven su propio peso, su propio nombre, etc.

Compuesto (Composite)

Los contenedores. Tienen hijos (Hojas u otros Compuestos). Implementan la operación delegando a sus hijos y combinando resultados.

La clave es que tanto la Hoja como el Compuesto implementan la misma interfaz. El código cliente solo conoce esa interfaz. No sabe ni le importa si está tratando con un archivo suelto o con una carpeta con 500 subcarpetas dentro.

¿Qué rol cumple la Hoja (Leaf) en el patrón Composite?


🧑‍💻 3. Ejemplo práctico en PHP: Sistema de archivos

Vamos a implementar el Composite con la analogía del explorador de archivos. El objetivo: poder llamar a mostrarEstructura() y calcularTamaño() en cualquier nodo del árbol.

<?php

// 1. El Componente (Interfaz común)
interface Elemento {
    public function mostrarEstructura(int $nivel = 0): void;
    public function calcularTamaño(): int; // en KB
    public function getNombre(): string;
}

// 2. La Hoja (Archivo)
class Archivo implements Elemento {
    private string $nombre;
    private int $tamaño; // KB

    public function __construct(string $nombre, int $tamaño) {
        $this->nombre = $nombre;
        $this->tamaño = $tamaño;
    }

    public function mostrarEstructura(int $nivel = 0): void {
        echo str_repeat('  ', $nivel) . "📄 {$this->nombre} ({$this->tamaño} KB)" . PHP_EOL;
    }

    public function calcularTamaño(): int {
        return $this->tamaño;
    }

    public function getNombre(): string {
        return $this->nombre;
    }
}

// 3. El Compuesto (Carpeta)
class Carpeta implements Elemento {
    private string $nombre;
    /** @var Elemento[] */
    private array $elementos = [];

    public function __construct(string $nombre) {
        $this->nombre = $nombre;
    }

    public function agregar(Elemento $elemento): void {
        $this->elementos[] = $elemento;
    }

    public function eliminar(string $nombre): void {
        $this->elementos = array_filter(
            $this->elementos,
            fn(Elemento $e) => $e->getNombre() !== $nombre
        );
    }

    public function mostrarEstructura(int $nivel = 0): void {
        echo str_repeat('  ', $nivel) . "📁 {$this->nombre}" . PHP_EOL;

        foreach ($this->elementos as $elemento) {
            $elemento->mostrarEstructura($nivel + 1);
        }
    }

    public function calcularTamaño(): int {
        $total = 0;
        foreach ($this->elementos as $elemento) {
            $total += $elemento->calcularTamaño();
        }
        return $total;
    }

    public function getNombre(): string {
        return $this->nombre;
    }

    public function contarElementos(): int {
        $total = 0;
        foreach ($this->elementos as $elemento) {
            if ($elemento instanceof Carpeta) {
                $total += $elemento->contarElementos();
            }
            $total++; // El elemento en sí
        }
        return $total;
    }
}

// 4. Uso del patrón
$documentos = new Carpeta('Documentos');
$documentos->agregar(new Archivo('curriculum.pdf', 250));
$documentos->agregar(new Archivo('contrato.docx', 180));

$imagenes = new Carpeta('Imágenes');
$imagenes->agregar(new Archivo('foto1.png', 1200));
$imagenes->agregar(new Archivo('logo.svg', 45));

$proyectos = new Carpeta('Proyectos');
$proyectos->agregar(new Archivo('index.php', 12));
$proyectos->agregar(new Archivo('style.css', 8));

$root = new Carpeta('Carpeta Raíz');
$root->agregar($documentos);
$root->agregar($imagenes);
$root->agregar($proyectos);

// 5. Operaciones uniformes
$root->mostrarEstructura();
echo PHP_EOL;
echo "Tamaño total: " . $root->calcularTamaño() . " KB" . PHP_EOL;
echo "Elementos totales: " . $root->contarElementos() . PHP_EOL;
echo "Solo Imágenes: " . $imagenes->calcularTamaño() . " KB" . PHP_EOL;

Salida:

📁 Carpeta Raíz
  📁 Documentos
    📄 curriculum.pdf (250 KB)
    📄 contrato.docx (180 KB)
  📁 Imágenes
    📄 foto1.png (1200 KB)
    📄 logo.svg (45 KB)
  📁 Proyectos
    📄 index.php (12 KB)
    📄 style.css (8 KB)

Tamaño total: 1695 KB
Elementos totales: 9
Solo Imágenes: 1245 KB

calcularTamaño() funciona igual en un Archivo (devuelve su tamaño) que en una Carpeta (suma recursivamente). El código cliente no distingue entre uno y otro. Llama al mismo método y chimpún.

¿Cómo calcula su tamaño una Carpeta (Compuesto) en el ejemplo?


🌍 4. Otros ejemplos reales

El Composite no es solo para sistemas de archivos, aparece en muchos sitios:

Menú de navegación

interface ElementoMenu {
    public function renderizar(): string;
}

class Enlace implements ElementoMenu {
    public function __construct(
        private string $texto,
        private string $url
    ) {}

    public function renderizar(): string {
        return "<a href='{$this->url}'>{$this->texto}</a>";
    }
}

class SubMenu implements ElementoMenu {
    private string $titulo;
    /** @var ElementoMenu[] */
    private array $items = [];

    public function __construct(string $titulo) {
        $this->titulo = $titulo;
    }

    public function agregar(ElementoMenu $item): void {
        $this->items[] = $item;
    }

    public function renderizar(): string {
        $html = "<li class='dropdown'>{$this->titulo}<ul>";
        foreach ($this->items as $item) {
            $html .= "<li>" . $item->renderizar() . "</li>";
        }
        $html .= "</ul></li>";
        return $html;
    }
}

// Uso
$menu = new SubMenu('Principal');
$menu->agregar(new Enlace('Inicio', '/'));
$menu->agregar(new Enlace('Blog', '/blog'));

$servicios = new SubMenu('Servicios');
$servicios->agregar(new Enlace('Desarrollo', '/dev'));
$servicios->agregar(new Enlace('Consultoría', '/consulting'));

$menu->agregar($servicios); // Un submenú dentro del menú
$menu->renderizar(); // Renderiza todo el árbol recursivamente

Otros casos donde lo ves

Componentes UI

React, Vue, Angular... toda la UI es un árbol de componentes. Un <Form> contiene <Input> y <Button>, y todo responde a render().

Organigrama empresarial

Cada departamento tiene subdepartamentos o empleados individuales. calcularCoste() funciona igual en un empleado que en toda la empresa.

Carrito de compra

Un producto tiene precio, pero un "pack" o "bundle" también. Ambos responden a getPrecio(). El pack suma recursivamente.

XML / HTML / JSON

Cualquier documento con estructura de árbol. Nodos que contienen nodos. toArray(), toString(), validate()... operaciones recursivas.


🔄 5. Comparativa con otros patrones

PatrónPropósitoDiferencia clave
CompositeTratar objetos individuales y grupos con la misma interfazEstructura en árbol; el Compuesto delega a sus hijos recursivamente
DecoratorAñadir funcionalidad a un objeto sin modificar su claseDecorator envuelve un solo objeto; Composite agrupa varios
IteratorRecorrer elementos de una colecciónIterator recorre la estructura; Composite la define
FlyweightCompartir estado entre muchos objetos similaresFlyweight optimiza memoria; Composite organiza jerarquía
Chain of ResponsibilityPasar petición por una cadena de manejadoresCoR es una cadena lineal; Composite es un árbol

Un dato interesante es que Composite y Iterator se llevan de maravilla. Puedes usar un Iterator para recorrer toda la estructura Composite de forma plana, sin que el cliente tenga que saber nada de la recursividad.


⚖️ 6. Relación con SOLID

SRP (Single Responsibility)

Archivo solo sabe de sí mismo. Carpeta solo sabe gestionar sus hijos y delegar. Cada clase tiene un solo motivo para cambiar.

OCP (Open/Closed)

¿Quieres añadir un Enlace simbólico? Crea una nueva clase que implemente Elemento y encájala en el árbol. Sin tocar nada existente.

DIP (Dependency Inversion)

El código cliente y la Carpeta dependen de la abstracción Elemento, no de Archivo o Carpeta concretamente. Inversión perfecta.

LSP (Liskov Substitution)

Donde el código espera un Elemento, puedes pasar un Archivo o una Carpeta indistintamente. El comportamiento es consistente.

¿Qué principio SOLID cumple al poder añadir un nuevo tipo de nodo (como EnlaceSimbolico) sin tocar el código existente?


✅ 7. Ventajas y desventajas

Ventajas
  • Uniformidad: El cliente trata Hojas y Compuestos igual. Cero if/else.
  • Recursividad natural: Las operaciones se propagan automáticamente por el árbol.
  • OCP: Añadir nuevos tipos de nodos sin tocar el código existente.
  • Profundidad ilimitada: Cajas dentro de cajas dentro de cajas... funciona igual.
  • Simplifica el cliente: Solo llama a un método, el árbol hace el resto.
Desventajas
  • Generalización forzada: Las Hojas pueden heredar métodos que no les aplican (agregar() en un Archivo).
  • Difícil restringir tipos: Es complicado limitar qué tipo de hijos acepta un Compuesto.
  • Debug: Seguir una operación recursiva por 10 niveles puede ser un lío.
  • Rendimiento: Árboles muy profundos con miles de nodos pueden ser costosos de recorrer.

¿Cuál es la principal desventaja del Composite respecto a la interfaz compartida?


⚠️ 8. Errores comunes

1. Poner agregar() y eliminar() en la interfaz

Es la tentación clásica: poner agregar() y eliminar() en el Componente. Pero una Hoja no puede tener hijos, así que esos métodos no tienen sentido en ella:

// ❌ MAL: la Hoja hereda métodos que no puede usar
interface Elemento {
    public function mostrarEstructura(): void;
    public function agregar(Elemento $e): void;  // ¿Un archivo con hijos? 🤔
    public function eliminar(string $nombre): void;
}

class Archivo implements Elemento {
    public function agregar(Elemento $e): void {
        throw new \RuntimeException("Un archivo no puede tener hijos");
    }
    // ...
}

// ✅ BIEN: solo el Compuesto tiene esos métodos
interface Elemento {
    public function mostrarEstructura(): void;
    public function calcularTamaño(): int;
}

class Carpeta implements Elemento {
    public function agregar(Elemento $e): void { /* ... */ }
    public function eliminar(string $nombre): void { /* ... */ }
    // ...
}

Este es el eterno debate del Composite: transparencia vs seguridad. Si pones agregar() en la interfaz, ganas transparencia (puedes tratar todo igual) pero pierdes seguridad (las Hojas tienen métodos inútiles). En la mayoría de casos, mejor dejarlo solo en el Compuesto.

2. Olvidar el caso base de la recursividad

Si la Hoja no implementa correctamente la operación, la recursividad nunca termina:

// ❌ MAL: la Hoja delega al padre (bucle infinito)
class Archivo implements Elemento {
    public function calcularTamaño(): int {
        return $this->padre->calcularTamaño(); // ¡Nunca termina!
    }
}

// ✅ BIEN: la Hoja ES el caso base
class Archivo implements Elemento {
    public function calcularTamaño(): int {
        return $this->tamaño; // Aquí para la recursividad
    }
}

3. Referencia circular padre-hijo

Si un Compuesto se agrega a sí mismo como hijo, tienes un bucle infinito:

// ❌ Esto es un stack overflow
$carpeta = new Carpeta('Loop');
$carpeta->agregar($carpeta); // 💀 La carpeta se contiene a sí misma
$carpeta->calcularTamaño(); // Recursividad infinita

Si quieres evitarlo, puedes validar en agregar():

public function agregar(Elemento $elemento): void {
    if ($elemento === $this) {
        throw new \InvalidArgumentException("No puedes agregar un elemento a sí mismo");
    }
    $this->elementos[] = $elemento;
}

¿Qué pasa si un Compuesto se agrega a sí mismo como hijo?

4. Usar instanceof en el cliente

Si el código cliente necesita saber si algo es Hoja o Compuesto, estás rompiendo el propósito del patrón:

// ❌ MAL: el cliente distingue entre tipos
foreach ($elementos as $e) {
    if ($e instanceof Carpeta) {
        $e->mostrarEstructura();
    } elseif ($e instanceof Archivo) {
        echo $e->getNombre();
    }
}

// ✅ BIEN: el cliente solo usa la interfaz
foreach ($elementos as $e) {
    $e->mostrarEstructura(); // Funciona igual para ambos
}

¿Por qué es un error usar instanceof en el código cliente para distinguir Hoja de Compuesto?


🤔 9. ¿Cuándo usarlo y cuándo no?

Úsalo cuando...
  • Tu problema tiene una estructura de árbol natural
  • Necesitas que el cliente trate objetos individuales y grupos de forma uniforme
  • Las operaciones son recursivas por naturaleza (sumar, buscar, renderizar...)
  • Quieres poder anidar elementos sin límite de profundidad
  • Trabajas con menús, UI, documentos, organigramas o carritos de compra
Evítalo cuando...
  • Tu estructura es plana (una lista simple, sin anidación)
  • Hoja y Compuesto son muy diferentes entre sí (interfaz común forzada)
  • Necesitas restricciones estrictas sobre qué tipo de hijos acepta cada nodo
  • Solo tienes un nivel de agrupación (un simple array basta)

Si estás modelando algo que se puede describir como “X puede contener otros X”, es un Composite pidiéndote a gritos que lo uses.

¿Cuándo NO deberías usar el patrón Composite?


💡 10. Conclusión

El Patrón Composite es la base de cualquier estructura jerárquica en software. Si tu problema tiene forma de árbol, Composite es tu patrón.

Su idea central es simple pero potente. Hojas y Compuestos implementan la misma interfaz. El cliente llama a un método y el árbol se encarga de propagarlo. Sin if/else, sin instanceof, sin conocer la profundidad.

Lo ves en los sistemas de archivos, en los frameworks de UI (React, Vue…), en los menús de navegación, en los documentos XML, en los organigramas empresariales… Donde hay una jerarquía de “cosas que contienen cosas”, hay un Composite escondido.

EA, ¡saluditos y nos vemos en los bares! 🍻