Patrón Decorator
Añade funciones a un objeto envolviéndolo en otro con la misma interfaz, que hace algo más antes o después de pasarle la llamada. Los decoradores se apilan en cualquier orden y combinación.
nivel intermedioTambién: decorador, wrapper, envoltorio
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Decorator
Elige la bebida y los extras en el orden en que se añaden: cada uno envuelve a lo anterior.
Paso 1
Se crea la bebida básica, un café de 1,20 €. Es la que van a envolver los decoradores.
1servir(new ConNata(new ConLeche(new Cafe())));
2
3class ConLeche extends Extra {
4 ConLeche(Bebida b) { super(b); }
5 public double precio() {
6 double dentro = bebida.precio();
7 return dentro + 0.30;
8 }
9}
10
11class Cafe implements Bebida {
12 public double precio() { return 1.20; } // capas = 1
13}Variables
- capas
- 1
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Una cafetería vende café y té, con leche, nata, sirope, doble o grande, en cualquier combinación. Con herencia haría falta una clase por combinación (CafeConLecheYNata, TeDobleConSirope…), que son cientos y crecen con cada extra nuevo.
El patrón Decorator envuelve el objeto en otro que tiene su misma interfaz (Bebida): el decorador guarda la bebida de dentro, le pasa las llamadas y añade lo suyo (+ " con leche", + 0,30 €). Como un decorador también es una Bebida, se puede envolver otra vez: new ConNata(new ConLeche(new Cafe())). Cada combinación se monta en tiempo de ejecución con unas pocas clases.
Los flujos de entrada y salida de Java están construidos así: BufferedReader(InputStreamReader(FileInputStream)) es una pila de decoradores, cada uno añade algo (búfer, conversión de bytes a caracteres) al de dentro. Los middlewares de Express y los filtros de los servlets siguen la misma idea.
Cuándo usarlo
- Hay que añadir responsabilidades a objetos concretos, no a toda la clase, y combinarlas libremente.
- La herencia daría una explosión de subclases (una por cada combinación de extras).
- Se quiere poder quitar o reordenar funciones (registro, caché, compresión, cifrado) sin tocar el objeto original.
Cuándo no
- Si solo hay una o dos variantes fijas: una subclase o un parámetro es más simple.
- Si el código depende de la clase concreta del objeto (
instanceof Cafe): un café decorado ya no es unCafe, y esa comprobación falla.
Participantes
- Componente. La interfaz común (
Bebida). - Componente concreto. El objeto básico que se va a decorar (
Cafe,Te). - Decorador base. Clase abstracta que implementa la interfaz y guarda el componente que envuelve (
Extra). - Decoradores concretos. Cada uno añade su comportamiento antes o después de delegar en el componente de dentro (
ConLeche,ConNata…).
Diagrama de clases
Ver el diagrama en PlantUML
1@startuml
2interface Bebida {
3 +descripcion() : String
4 +precio() : double
5}
6class Cafe
7class Te
8abstract class Extra {
9 #bebida : Bebida
10 +Extra(bebida : Bebida)
11}
12class ConLeche
13class ConNata
14class ConSirope {
15 -sabor : String
16}
17Bebida <|.. Cafe
18Bebida <|.. Te
19Bebida <|.. Extra
20Extra <|-- ConLeche
21Extra <|-- ConNata
22Extra <|-- ConSirope
23Extra o-- "1" Bebida : envuelve
24@endumlPuedes copiarlo en el editor de diagramas UML y modificarlo.
El código
Los extras de una cafetería
Cuatro clases dan todas las combinaciones posibles: cada decorador añade su parte al resultado del de dentro.
1import java.util.*;
2
3interface Bebida {
4 String descripcion();
5 double precio();
6}
7
8class Cafe implements Bebida {
9 public String descripcion() { return "Café"; }
10 public double precio() { return 1.20; }
11}
12
13/** El decorador base: es una Bebida y envuelve a otra Bebida, a la que le pasa las llamadas. */
14abstract class Extra implements Bebida {
15 protected final Bebida bebida;
16 Extra(Bebida bebida) { this.bebida = bebida; }
17}
18
19class ConLeche extends Extra {
20 ConLeche(Bebida b) { super(b); }
21 public String descripcion() { return bebida.descripcion() + " con leche"; } // añade algo
22 public double precio() { return bebida.precio() + 0.30; } // y delega el resto
23}
24
25class ConNata extends Extra {
26 ConNata(Bebida b) { super(b); }
27 public String descripcion() { return bebida.descripcion() + " con nata"; }
28 public double precio() { return bebida.precio() + 0.50; }
29}
30
31class ConSirope extends Extra {
32 private final String sabor;
33 ConSirope(Bebida b, String sabor) { super(b); this.sabor = sabor; }
34 public String descripcion() { return bebida.descripcion() + " con sirope de " + sabor; }
35 public double precio() { return bebida.precio() + 0.40; }
36}
37
38public class Main {
39 static void servir(Bebida b) {
40 System.out.println(b.descripcion() + ": " + String.format(Locale.ROOT, "%.2f", b.precio()).replace('.', ',') + " €");
41 }
42
43 public static void main(String[] args) {
44 servir(new Cafe());
45 servir(new ConLeche(new Cafe()));
46 servir(new ConNata(new ConLeche(new Cafe()))); // los decoradores se apilan
47 servir(new ConSirope(new ConSirope(new Cafe(), "vainilla"), "caramelo")); // y se repiten
48 }
49}from abc import ABC, abstractmethod
class Bebida(ABC):
@abstractmethod
def descripcion(self): ...
@abstractmethod
def precio(self): ...
class Cafe(Bebida):
def descripcion(self):
return "Café"
def precio(self):
return 1.20
class Extra(Bebida):
"""El decorador base: es una Bebida y envuelve a otra Bebida, a la que le pasa las llamadas."""
def __init__(self, bebida):
self.bebida = bebida
class ConLeche(Extra):
def descripcion(self):
return self.bebida.descripcion() + " con leche" # añade algo
def precio(self):
return self.bebida.precio() + 0.30 # y delega el resto
class ConNata(Extra):
def descripcion(self):
return self.bebida.descripcion() + " con nata"
def precio(self):
return self.bebida.precio() + 0.50
class ConSirope(Extra):
def __init__(self, bebida, sabor):
super().__init__(bebida)
self.sabor = sabor
def descripcion(self):
return f"{self.bebida.descripcion()} con sirope de {self.sabor}"
def precio(self):
return self.bebida.precio() + 0.40
def servir(b):
print(f"{b.descripcion()}: {b.precio():.2f} €".replace(".", ","))
servir(Cafe())
servir(ConLeche(Cafe()))
servir(ConNata(ConLeche(Cafe()))) # los decoradores se apilan
servir(ConSirope(ConSirope(Cafe(), "vainilla"), "caramelo")) # y se repitenclass Cafe {
descripcion() { return "Café"; }
precio() { return 1.20; }
}
/** El decorador base: tiene los mismos métodos que una bebida y envuelve a otra. */
class Extra {
constructor(bebida) { this.bebida = bebida; }
}
class ConLeche extends Extra {
descripcion() { return this.bebida.descripcion() + " con leche"; } // añade algo
precio() { return this.bebida.precio() + 0.30; } // y delega el resto
}
class ConNata extends Extra {
descripcion() { return this.bebida.descripcion() + " con nata"; }
precio() { return this.bebida.precio() + 0.50; }
}
class ConSirope extends Extra {
constructor(bebida, sabor) { super(bebida); this.sabor = sabor; }
descripcion() { return `${this.bebida.descripcion()} con sirope de ${this.sabor}`; }
precio() { return this.bebida.precio() + 0.40; }
}
function servir(b) {
console.log(`${b.descripcion()}: ${b.precio().toFixed(2).replace(".", ",")} €`);
}
servir(new Cafe());
servir(new ConLeche(new Cafe()));
servir(new ConNata(new ConLeche(new Cafe()))); // los decoradores se apilan
servir(new ConSirope(new ConSirope(new Cafe(), "vainilla"), "caramelo")); // y se repitenusing System;
using System.Globalization;
interface IBebida {
string Descripcion();
decimal Precio();
}
class Cafe : IBebida {
public string Descripcion() => "Café";
public decimal Precio() => 1.20m;
}
// El decorador base: es una IBebida y envuelve a otra IBebida, a la que le pasa las llamadas.
abstract class Extra : IBebida {
protected readonly IBebida bebida;
protected Extra(IBebida bebida) => this.bebida = bebida;
public abstract string Descripcion();
public abstract decimal Precio();
}
class ConLeche : Extra {
public ConLeche(IBebida b) : base(b) { }
public override string Descripcion() => bebida.Descripcion() + " con leche"; // añade algo
public override decimal Precio() => bebida.Precio() + 0.30m; // y delega el resto
}
class ConNata : Extra {
public ConNata(IBebida b) : base(b) { }
public override string Descripcion() => bebida.Descripcion() + " con nata";
public override decimal Precio() => bebida.Precio() + 0.50m;
}
class ConSirope : Extra {
private readonly string sabor;
public ConSirope(IBebida b, string sabor) : base(b) => this.sabor = sabor;
public override string Descripcion() => quot;{bebida.Descripcion()} con sirope de {sabor}";
public override decimal Precio() => bebida.Precio() + 0.40m;
}
class Program {
static void Servir(IBebida b) =>
Console.WriteLine(quot;{b.Descripcion()}: {b.Precio().ToString("F2", CultureInfo.InvariantCulture).Replace('.', ',')} €");
static void Main() {
Servir(new Cafe());
Servir(new ConLeche(new Cafe()));
Servir(new ConNata(new ConLeche(new Cafe()))); // los decoradores se apilan
Servir(new ConSirope(new ConSirope(new Cafe(), "vainilla"), "caramelo")); // y se repiten
}
}<?php
interface Bebida {
public function descripcion(): string;
public function precio(): float;
}
class Cafe implements Bebida {
public function descripcion(): string { return "Café"; }
public function precio(): float { return 1.20; }
}
/** El decorador base: es una Bebida y envuelve a otra Bebida, a la que le pasa las llamadas. */
abstract class Extra implements Bebida {
public function __construct(protected Bebida $bebida) {}
}
class ConLeche extends Extra {
public function descripcion(): string { return $this->bebida->descripcion() . " con leche"; } // añade algo
public function precio(): float { return $this->bebida->precio() + 0.30; } // y delega el resto
}
class ConNata extends Extra {
public function descripcion(): string { return $this->bebida->descripcion() . " con nata"; }
public function precio(): float { return $this->bebida->precio() + 0.50; }
}
class ConSirope extends Extra {
public function __construct(Bebida $bebida, private string $sabor) { parent::__construct($bebida); }
public function descripcion(): string { return $this->bebida->descripcion() . " con sirope de {$this->sabor}"; }
public function precio(): float { return $this->bebida->precio() + 0.40; }
}
function servir(Bebida $b): void {
echo $b->descripcion(), ": ", number_format($b->precio(), 2, ",", ""), " €\n";
}
servir(new Cafe());
servir(new ConLeche(new Cafe()));
servir(new ConNata(new ConLeche(new Cafe()))); // los decoradores se apilan
servir(new ConSirope(new ConSirope(new Cafe(), "vainilla"), "caramelo")); // y se repitenSalida al ejecutarlo (la misma en los 5 lenguajes)
Café: 1,20 € Café con leche: 1,50 € Café con leche con nata: 2,00 € Café con sirope de vainilla con sirope de caramelo: 2,00 €
Decoradores de la biblioteca
La entrada/salida y las colecciones de Java usan decoradores todo el rato.
1// La entrada/salida de Java es una pila de decoradores: cada capa añade algo a la de dentro
2try (BufferedReader lector = new BufferedReader( // + lectura por líneas y búfer
3 new InputStreamReader( // + bytes → caracteres (UTF-8)
4 new GZIPInputStream( // + descomprime
5 new FileInputStream("datos.txt.gz")), // el flujo de bytes del fichero
6 StandardCharsets.UTF_8))) {
7 String linea;
8 while ((linea = lector.readLine()) != null) System.out.println(linea);
9}
10
11// Collections envuelve una lista en otra con la misma interfaz y un comportamiento añadido
12List<String> soloLectura = Collections.unmodifiableList(lista); // add() lanza una excepción
13List<String> segura = Collections.synchronizedList(lista); // cada método se sincronizaimport gzip
import io
# Los flujos de Python también se apilan: texto UTF-8 sobre gzip sobre el fichero
with open("datos.txt.gz", "rb") as fichero:
with io.TextIOWrapper(gzip.GzipFile(fileobj=fichero), encoding="utf-8") as lector:
for linea in lector:
print(linea, end="")
# Los decoradores de funciones (@) aplican la misma idea a una función: la envuelven en otra
from functools import lru_cache
@lru_cache # añade una caché sin tocar el código de la función
def fibonacci(n):
return n if n < 2 else fibonacci(n - 1) + fibonacci(n - 2)// Una función que envuelve a otra y le añade algo: el decorador de JavaScript de todos los días
function conRegistro(fn) {
return (...args) => {
console.log(`Llamada a ${fn.name}(${args.join(", ")})`);
return fn(...args);
};
}
const sumarRegistrada = conRegistro((a, b) => a + b);
// Los middlewares de Express son una pila de capas alrededor de cada petición
app.use(express.json()); // + lee el cuerpo JSON
app.use(compression()); // + comprime la respuesta
app.use(morgan("dev")); // + registra cada petición// Los Stream de .NET se apilan igual que en Java
using var fichero = File.OpenRead("datos.txt.gz");
using var descomprimido = new GZipStream(fichero, CompressionMode.Decompress); // + descomprime
using var conBufer = new BufferedStream(descomprimido); // + búfer
using var lector = new StreamReader(conBufer, Encoding.UTF8); // + texto UTF-8
Console.WriteLine(lector.ReadToEnd());
// ASP.NET Core: cada middleware envuelve al siguiente
app.UseHttpsRedirection();
app.UseAuthentication();<?php
// Los filtros de flujo de PHP se añaden sobre un flujo abierto
$f = fopen("compress.zlib://datos.txt.gz", "r"); // + descomprime
stream_filter_append($f, "string.toupper"); // + mayúsculas
echo stream_get_contents($f);
// En Laravel, los middlewares envuelven cada petición: autenticación, CSRF, registro…
Route::get("/notas", [NotaController::class, "index"])->middleware(["auth", "throttle:60,1"]);En la práctica
- Los flujos de Java:
BufferedReader,InputStreamReader,GZIPInputStream,DataOutputStream…, todos envuelven a otro flujo. Collections.unmodifiableListyCollections.synchronizedList: la misma lista con un comportamiento añadido.- Los middlewares de Express y de Laravel y los filtros de los servlets: cada capa hace algo con la petición y llama a la siguiente.
- Los decoradores de Python (
@lru_cache,@login_required) y las anotaciones de Spring como@Transactionalo@Cacheableaplican la misma idea a funciones y métodos.
Errores típicos
- Olvidar llamar al objeto envuelto: el decorador «se come» la llamada y el resto de la pila no hace nada.
- Depender del orden sin darse cuenta: numerar y después quitar líneas vacías no da lo mismo que al revés.
- Usar
instanceofo comparar clases con objetos decorados: el de fuera es el decorador, no el original. - Crear decoradores que cambian la interfaz (añaden métodos públicos que el resto no tiene): ya no se pueden apilar.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. Pedidos de la cafetería
Cada línea es una bebida con sus extras, en el orden en que se añaden. Ya está ConLeche. Escribe los decoradores ConNata, Doble, ConSirope y Grande y complétalos en anadir.
- Cada línea:
cafeoteseguido de extras:leche(+0,30 €, « con leche»),nata(+0,50 €, « con nata»),doble(precio × 2 − 0,20 €, « doble»),sirope:sabor(+0,40 €, « con sirope de sabor») ygrande(precio × 1,25, « (grande)»). - Salida:
descripción: precio €por bebida y, al final,N bebidas, total X €. Errores:Bebida desconocida: xyExtra desconocido: x(tambiénsirope:sin sabor).
Ejemplo
cafe cafe leche cafe leche nata te sirope:menta cafe doble grande cafe grande doble
Café: 1,20 € Café con leche: 1,50 € Café con leche con nata: 2,00 € Té con sirope de menta: 1,50 € Café doble (grande): 2,75 € Café (grande) doble: 2,80 € 6 bebidas, total 11,75 €