Diseño de software

Patrón Composite en Swift: tratar una estructura de árbol como un solo objeto

Una carpeta dentro de otra, con más archivos dentro. Si has trabajado con este tipo de estructura de árbol en código, seguro que alguna vez te ha resultado abrumadora.

4 min de lectura
Imagen de portada de Patrón Composite en Swift: tratar una estructura de árbol como un solo objeto

Una carpeta dentro de otra, con más archivos dentro. Si has trabajado con este tipo de estructura de árbol en código, seguro que alguna vez te ha resultado abrumadora.

Si divides todo con if para preguntar «¿Esto es una carpeta o un archivo?», el código se ensucia enseguida.

Es un problema habitual al crear algo parecido a un explorador de archivos. En estos casos, la herramienta adecuada es el patrón Composite.

El patrón Composite trata los objetos individuales (archivos) y sus agrupaciones (carpetas) como el mismo tipo, de modo que toda la estructura de árbol se maneja como un solo objeto.

Hoy veremos cómo resolver este patrón en Swift con código que he escrito personalmente.

Resumen clave

  1. Patrón Composite = agrupar hojas y composites bajo el mismo protocolo
  2. El cliente realiza la misma llamada sin distinguir entre archivos y carpetas
  3. El cálculo de una carpeta pregunta de nuevo a sus hijos mediante una estructura recursiva
  4. En Swift se puede implementar limpiamente con solo protocol

¿Qué es el patrón Composite?

Literalmente, es «composición»: reunir elementos pequeños para formar una unidad grande.

La analogía más habitual es un sistema de archivos.

Un archivo es una hoja que termina en sí misma. Una carpeta, en cambio, es una rama que puede contener archivos u otras carpetas.

Una carpeta dentro de otra: esta estructura es Composite
Una carpeta dentro de otra: esta estructura es Composite

Pero la perspectiva del usuario es distinta. Tanto si abre una carpeta como un archivo, solo damos la orden «Abrir».

El objetivo del patrón Composite es tratar los elementos individuales y las agrupaciones de la misma forma.

Los componentes se dividen en tres tipos.

Visto en un diagrama, es así de sencillo
Visto en un diagrama, es así de sencillo
  • Component: interfaz común (protocolo)
  • Leaf: objeto terminal sin hijos (archivo)
  • Composite: objeto que contiene hijos (carpeta)

¿Cómo se implementa en Swift?

Primero definimos el protocolo común para poder consultar el nombre y el tamaño.

Estas son las reglas (Component) que siguen tanto los archivos como las carpetas.

protocol FileComponent {
    var name: String { get }
    func size() -> Int   // devuelve el tamaño en bytes
}

Este es el archivo que corresponde a la hoja. Solo tiene que devolver su propio tamaño, así que es sencillo.

struct File: FileComponent {
    let name: String
    let bytes: Int
    func size() -> Int { bytes }  // devuelve su propio tamaño sin cambios
}

La clave es la carpeta (Composite). Guarda sus hijos en un array y, cuando se consulta su tamaño, vuelve a preguntárselo a ellos.

struct Folder: FileComponent {
    let name: String
    var children: [FileComponent] = []
    func size() -> Int {
        children.reduce(0) { $0 + $1.size() }  // suma de los hijos (recursiva)
    }
}

Lo importante es que size()se vuelve a llamar a sí mismo, formando una estructura recursiva.

Sin importar cuántos niveles de carpetas haya, esta línea recorre todo automáticamente hasta el fondo.


Así se ve en la práctica

Ahora crearemos un árbol mezclando archivos y carpetas.

let root = Folder(name: "Documentos", children: [
    File(name: "Notas.txt", bytes: 100),
    Folder(name: "Fotos", children: [
        File(name: "Viajes.jpg", bytes: 2000)
    ])
])
print(root.size())  // 2100 imprimir

¿Lo ves? Con una sola llamada a root.size() se suman los tamaños de todos los archivos de la carpeta.

Esta fue la parte que más me impresionó al ejecutarlo personalmente.

En el código cliente nunca preguntamos «¿Esto es una carpeta o un archivo?».

Solo hay que llamar a size(). Cada objeto se encarga del resto.

Pegué el ejemplo tal cual en un Playground y lo ejecuté
Pegué el ejemplo tal cual en un Playground y lo ejecuté

¿En qué se diferencia de procesarlo con condicionales?

Quizá pienses: «¿No puedo dividir los tipos con una sentencia if y procesarlos?»

Sí, puedes. Pero la diferencia aumenta mucho a medida que el árbol se hace más profundo.

Categoría Método con condicionales (if/switch) Patrón Composite
Identificación del tipo Comprobarlo manualmente cada vez No es necesario
Añadir un tipo nuevo Modificar condicionales en varios lugares Adoptar el protocolo
Procesamiento recursivo Escribir el código de recorrido manualmente El objeto se encarga
Legibilidad del código Más compleja cuanto más profundo Se mantiene uniforme

Con el enfoque condicional, al añadir un nuevo tipo (por ejemplo, un enlace directo), hay que volver a agregar if en varios lugares.

Con Composite basta con crear un nuevo tipo que adopte FileComponent. No hace falta tocar el código existente.


Preguntas frecuentes

P. ¿Las interfaces de Leaf y Composite deben ser exactamente iguales?

Idealmente, sí. Sin embargo, una función como «añadir hijo» resulta extraña para un archivo. En ese caso, conviene dejar en el protocolo solo los comportamientos comunes (como size) y gestionar los hijos únicamente en las carpetas.

P. ¿Puedo usar class en lugar de struct?

Sí, como en el ejemplo anterior, también funciona con un tipo por valor. Pero si modificas el árbol con frecuencia y necesitas compartir referencias, class puede resultar más cómodo.


Si las estructuras de árbol te han sumido en el infierno de if, prueba el patrón Composite.

Encaja bien no solo en sistemas de archivos, sino también en jerarquías de vistas, menús y organigramas: cualquier estructura con «más elementos dentro».

Pega el ejemplo de hoy tal cual en un Playground y ejecútalo; la idea quedará clara enseguida.

Lecturas recomendadas