El Composite es un patrón que trata objetos individuales y composiciones mediante una interfaz común. Resulta especialmente útil para modelar estructuras de árbol.
En el desarrollo de software, a menudo necesitamos modelar jerarquías: menús con submenús, sistemas de archivos, la estructura HTML (DOM), o gráficos vectoriales donde agrupas formas.
El problema surge cuando queremos realizar una operación sobre toda la estructura, pero los “nodos” (hojas) y los “contenedores” (ramas) son clases distintas.
El Problema: El Pedido Complejo
Imagina que trabajamos en la logística de una tienda online. Tenemos que calcular el precio total de un envío.
Un envío puede ser simple:
- Un
Producto(ej: Unos auriculares - 50€).
O puede ser complejo:
- Una
Cajaque contiene: - Un
Producto(Martillo). - Un
Producto(Clavos). - Otra
Caja(pack de herramientas pequeñas) que contiene: - Destornillador.
- Cinta métrica.
Si no usamos patrones, nuestro código de cálculo de precio sería una pesadilla de comprobaciones de tipos:
// ❌ Código doloroso sin Composite
public double CalcularPrecio(object item)
{
if (item is Producto p)
{
return p.Precio;
}
else if (item is Caja c)
{
double total = 0;
foreach (var subItem in c.Contenido)
{
// ¡Recursividad manual y comprobación de tipos constante!
total += CalcularPrecio(subItem);
}
return total;
}
}Esto viola el principio Abierto/Cerrado. Si mañana añadimos un “Palet” o un “Contenedor”, hay que reescribir la lógica de cálculo.
La Solución: “Trátalos a todos igual”
El patrón Composite sugiere que creemos una clase base común (o interfaz) tanto para los elementos individuales (Hoja) como para los contenedores (Composite).
De esta forma, al contenedor no le importa si lo que tiene dentro es un producto simple o una caja gigante. Él solo llama al método GetPrecio() y deja que el polimorfismo haga su trabajo.
Implementación en C#
Vamos a resolver el problema del envío logístico.
Definimos la clase abstracta que representa cualquier cosa que se pueda enviar.
public abstract class ItemEnvio
{
protected string _nombre;
public ItemEnvio(string nombre)
{
_nombre = nombre;
}
// La operación común
public abstract double GetPrecio();
// Operaciones opcionales para gestionar hijos
// (Podemos dejarlas vacías o lanzar error por defecto)
public virtual void Add(ItemEnvio item)
{
throw new NotImplementedException();
}
public virtual void Remove(ItemEnvio item)
{
throw new NotImplementedException();
}
}El producto simple. No tiene hijos, solo precio.
public class Producto : ItemEnvio
{
private double _precio;
public Producto(string nombre, double precio) : base(nombre)
{
_precio = precio;
}
public override double GetPrecio()
{
Console.WriteLine($" - Producto: {_nombre} ({_precio}€)");
return _precio;
}
}La caja. Fíjate en el detalle del método GetPrecio: usa recursividad implícita.
public class Caja : ItemEnvio
{
// La clave del patrón: Una lista de TIPOS BASE
private List<ItemEnvio> _contenido = new List<ItemEnvio>();
public Caja(string nombre) : base(nombre) { }
public override void Add(ItemEnvio item)
{
_contenido.Add(item);
}
public override void Remove(ItemEnvio item)
{
_contenido.Remove(item);
}
public override double GetPrecio()
{
Console.WriteLine($"Abriendo caja: {_nombre}...");
double precioTotal = 0;
// Recorremos los hijos. NO sabemos si son cajas o productos.
// Y no nos importa. Solo sabemos que tienen GetPrecio().
foreach (var item in _contenido)
{
precioTotal += item.GetPrecio();
}
// Supongamos que la caja en sí cuesta 1€ por el cartón
return precioTotal + 1.0;
}
}Podemos crear estructuras tan profundas como queramos.
class Program
{
static void Main(string[] args)
{
// 1. Creamos productos sueltos
var auriculares = new Producto("Auriculares Sony", 50);
var cargador = new Producto("Cargador USB", 15);
var martillo = new Producto("Martillo Thor", 25);
// 2. Creamos una caja pequeña (Composite)
var cajaTecnologia = new Caja("Pack Tech");
cajaTecnologia.Add(auriculares);
cajaTecnologia.Add(cargador);
// 3. Creamos una caja GRANDE que contiene productos y OTRA CAJA
var pedidoFinal = new Caja("Pedido Amazon");
pedidoFinal.Add(martillo); // Añadimos una Hoja
pedidoFinal.Add(cajaTecnologia); // Añadimos un Composite
// 4. Calculamos el total
Console.WriteLine("--- Calculando Total del Pedido ---");
double total = pedidoFinal.GetPrecio();
Console.WriteLine($"\nPrecio Total a pagar: {total}€");
}
}Salida:
--- Calculando Total del Pedido ---
Abriendo caja: Pedido Amazon...
- Producto: Martillo Thor (25€)
Abriendo caja: Pack Tech...
- Producto: Auriculares Sony (50€)
- Producto: Cargador USB (15€)
Precio Total a pagar: 92€(90€ de productos + 1€ caja pequeña + 1€ caja grande)
¿Por qué es tan potente?
Lo mejor de este patrón es que simplifica enormemente al cliente. El cliente no necesita preocuparse de recorrer el árbol. Llama al método en la raíz, y la llamada se propaga en cascada por todas las ramas hasta llegar a las últimas hojas.
Casos de uso típicos
- Sistemas Gráficos: Un “Grupo” de formas en PowerPoint. Puedes mover el grupo, o escalar el grupo, y todas las líneas y rectángulos de dentro se mueven.
- Sistemas de Archivos:
GetSize()oDelete()funcionan igual para carpetas y archivos. - Interfaces de Usuario (WPF/WinForms): Un
PanelcontieneBotonesy otrosPaneles. Cuando renderizas el panel principal, se renderizan todos sus hijos.
El patrón Composite nos permite crear estructuras complejas a partir de objetos simples, manteniendo una interfaz uniforme. Es la definición de elegancia recursiva.