Al desarrollar para iOS, a veces quieres conservar la funcionalidad básica y añadir solo logging o caché. Si empiezas a heredar clases para hacerlo, el árbol de herencia pronto se convierte en un desastre.
Al añadir funciones una a una a un cliente de red, es fácil acabar creando una clase con un nombre como CachingLoggingRetryingClient.
Hoy veremos el patrón Decorator de Swift, que resuelve este problema: añadir funciones por capas, como una cebolla, sin herencia.
El patrón Decorator envuelve un objeto con otro que sigue el mismo protocolo y añade comportamiento por capas sin modificar el código original.
La conclusión es esta.
- Define primero el comportamiento común mediante un protocolo
- El decorator sigue ese protocolo y contiene dentro otro valor del mismo tipo de protocolo
- Hace su propio trabajo, como logging o caché, y delega el resto en el objeto envuelto
- Sigue envolviendo según sea necesario para acumular funciones
¿Qué es el patrón Decorator? ¿En qué se diferencia de la herencia?
La herencia expresa una relación «es un tipo de». El hijo hereda todo del padre.
El problema son las combinaciones. Si creas clientes con logging, caché o ambos mediante herencia, cada combinación genera otra clase.
Un decorator expresa una relación de «algo envuelto».
Como el objeto envolvente y el envuelto comparten la misma interfaz, desde fuera parecen iguales. El código cliente no cambia, sin importar cuántas capas añadas.
En resumen, la herencia fija las relaciones en compilación, mientras que los decorators se pueden combinar libremente en tiempo de ejecución.
Implementación del patrón Decorator de Swift con código
Creemos un DataLoader que cargue datos. Primero, este es el protocolo que todos seguirán.
protocol DataLoader {
func load(id: String) -> Data?
}
// Implementación básica que realiza el trabajo real
struct NetworkLoader: DataLoader {
func load(id: String) -> Data? {
print("Petición \(id) a la red")
return Data("payload-\(id)".utf8)
}
}
Hasta aquí solo tenemos un protocolo y una implementación normales.
Ahora viene lo importante. El decorator sigue DataLoader y contiene dentro otro DataLoader.
// Decorator de logging
struct LoggingLoader: DataLoader {
let wrapped: DataLoader // Objeto envuelto
func load(id: String) -> Data? {
print("[Inicio] load del log: \(id)")
let result = wrapped.load(id: id) // Delegar el trabajo real
print("[Fin] load del log: \(id)")
return result
}
}
LoggingLoader solo hace su trabajo, registrar el log, y delega la carga real en wrapped.
Gracias a esta estructura, no importa qué haya dentro. Solo tiene que ser DataLoader.
Acumular funciones capa por capa
Añadamos otro decorator de caché para ver cómo se combinan de verdad.
final class CachingLoader: DataLoader {
let wrapped: DataLoader
private var cache: [String: Data] = [:]
init(wrapping loader: DataLoader) { self.wrapped = loader }
func load(id: String) -> Data? {
if let hit = cache[id] { return hit } // Devolver de inmediato si está en la caché
let data = wrapped.load(id: id) // Delegar si no está
cache[id] = data
return data
}
}
La parte de ensamblaje es donde el patrón Decorator demuestra todo su valor.
let loader = LoggingLoader(
wrapped: CachingLoader(wrapping: NetworkLoader())
)
Hay que leerlo desde dentro hacia fuera. Envolvimos el cargador de red con caché y después lo envolvimos con logging.
Cuando llega una llamada, el flujo es logging → comprobar la caché → red si no existe.
Para cambiar el orden, basta con cambiar el orden de los envoltorios. Ni NetworkLoader ni CachingLoader requieren modificar una línea de código.
Si una función no hace falta, basta con quitar esa capa.
Herencia o decorators: ¿cuál conviene usar?
Ninguna opción es siempre correcta. Veamos cada situación.
| Situación | Recomendación |
|---|---|
| Relación clara de «es un tipo de» | Herencia |
| Combinar y quitar funciones libremente | Decorator |
| Activar y desactivar funciones en tiempo de ejecución | Decorator |
| Cuando no se puede modificar el código original | Decorator |
| Muchas combinaciones posibles | Decorator |
Los decorators encajan especialmente bien con funciones transversales, no con el comportamiento esencial, como logging, caché, reintentos y adición de cabeceras de autenticación.
Si fuerzas el uso de decorators, las capas pueden multiplicarse y el call stack hacerse más profundo durante la depuración. Conviene tenerlo en cuenta.
Dos preguntas frecuentes
P. ¿Debo usar un struct o una class?
Si el decorator debe mantener estado, como una caché, una class resulta cómoda. Para una delegación simple, un struct es suficiente. Por eso en el ejemplo solo la caché es una class.
P. ¿Hay problemas de rendimiento si aumentan las capas?
Como las llamadas atraviesan cada capa, existe una sobrecarga mínima. Frente a la E/S de red o disco, es insignificante en la mayoría de las apps.
En esencia, el patrón Decorator consiste en definir un protocolo, crear un objeto que contenga el mismo protocolo y delegar a través de él.
La próxima vez que rebusques en el árbol de herencia para añadir una función, piensa en envolverla con otra capa. El código será mucho más ligero. Te recomiendo trasladar este ejemplo tal cual a un playground y probarlo 🙂

