Apuntes DAM

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.

leche, nata, sirope, doble o grande

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 un Cafe, y esa comprobación falla.

Participantes

  1. Componente. La interfaz común (Bebida).
  2. Componente concreto. El objeto básico que se va a decorar (Cafe, Te).
  3. Decorador base. Clase abstracta que implementa la interfaz y guarda el componente que envuelve (Extra).
  4. Decoradores concretos. Cada uno añade su comportamiento antes o después de delegar en el componente de dentro (ConLeche, ConNata…).

Diagrama de clases

1envuelve«interface»Bebida+descripcion() : String+precio() : doubleCafeTe«abstract»Extra#bebida : Bebida+Extra(bebida : Bebida)ConLecheConNataConSirope-sabor : String
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
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@enduml

Puedes 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.

Java
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}

Salida 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.

Java
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 sincroniza

En la práctica

  • Los flujos de Java: BufferedReader, InputStreamReader, GZIPInputStream, DataOutputStream…, todos envuelven a otro flujo.
  • Collections.unmodifiableList y Collections.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 @Transactional o @Cacheable aplican 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 instanceof o 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: cafe o te seguido 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») y grande (precio × 1,25, « (grande)»).
  • Salida: descripción: precio € por bebida y, al final, N bebidas, total X €. Errores: Bebida desconocida: x y Extra desconocido: x (también sirope: sin sabor).
JavaPedidos de la cafeteríaFácil

Ejemplo

Entrada (lo que se escribe por teclado)
cafe
cafe leche
cafe leche nata
te sirope:menta
cafe doble grande
cafe grande doble
Salida esperada
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 €
Test oculto #3
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
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
13class Te implements Bebida {
14    public String descripcion() { return "Té"; }
15    public double precio() { return 1.10; }
16}
17
18/** El decorador base: es una Bebida y envuelve a otra Bebida. */
19abstract class Extra implements Bebida {
20    protected final Bebida bebida;
21    Extra(Bebida bebida) { this.bebida = bebida; }
22}
23
24class ConLeche extends Extra {
25    ConLeche(Bebida b) { super(b); }
26    public String descripcion() { return bebida.descripcion() + " con leche"; }
27    public double precio() { return bebida.precio() + 0.30; }
28}
29
30class ConNata extends Extra {
31    ConNata(Bebida b) { super(b); }
32    public String descripcion() { return bebida.descripcion() + " con nata"; }
33    public double precio() { return bebida.precio() + 0.50; }
34}
35
36class Doble extends Extra {
37    Doble(Bebida b) { super(b); }
38    public String descripcion() { return bebida.descripcion() + " doble"; }
39    public double precio() { return bebida.precio() * 2 - 0.20; }
40}
41
42class ConSirope extends Extra {
43    private final String sabor;
44    ConSirope(Bebida b, String sabor) { super(b); this.sabor = sabor; }
45    public String descripcion() { return bebida.descripcion() + " con sirope de " + sabor; }
46    public double precio() { return bebida.precio() + 0.40; }
47}
48
49class Grande extends Extra {
50    Grande(Bebida b) { super(b); }
51    public String descripcion() { return bebida.descripcion() + " (grande)"; }
52    public double precio() { return bebida.precio() * 1.25; }
53}
54
55public class Main {
56    static String euros(double x) {
57        return String.format(Locale.ROOT, "%.2f", x).replace('.', ',') + " €";
58    }
59
60    /** Envuelve la bebida con el extra que pide la palabra, o lanza una excepción. */
61    static Bebida anadir(Bebida b, String extra) {
62        if (extra.startsWith("sirope:") && extra.length() > 7) return new ConSirope(b, extra.substring(7));
63        return switch (extra) {
64            case "leche" -> new ConLeche(b);
65            case "nata" -> new ConNata(b);
66            case "doble" -> new Doble(b);
67            case "grande" -> new Grande(b);
68            default -> throw new IllegalArgumentException("Extra desconocido: " + extra);
69        };
70    }
71
72    public static void main(String[] args) {
73        Scanner sc = new Scanner(System.in);
74        double total = 0;
75        int servidas = 0;
76        while (sc.hasNextLine()) {
77            String[] p = sc.nextLine().trim().split("\\s+");
78            if (p[0].isEmpty()) continue;
79            try {
80                Bebida b = switch (p[0]) {
81                    case "cafe" -> new Cafe();
82                    case "te" -> new Te();
83                    default -> throw new IllegalArgumentException("Bebida desconocida: " + p[0]);
84                };
85                for (int i = 1; i < p.length; i++) b = anadir(b, p[i]);
86                System.out.println(b.descripcion() + ": " + euros(b.precio()));
87                total += b.precio();
88                servidas++;
89            } catch (IllegalArgumentException e) {
90                System.out.println(e.getMessage());
91            }
92        }
93        System.out.println(servidas + " bebidas, total " + euros(total));
94    }
95}

Con cinco decoradores salen todas las combinaciones, repetidas y en cualquier orden, sin ninguna clase por combinación.

Un decorador solo conoce la interfaz de lo que envuelve: no sabe si dentro hay un café, un té u otros tres decoradores.

2. Filtros de texto apilables

La primera línea dice qué filtros se aplican al texto que viene después, y en qué orden. Cada filtro es un decorador de Salida que recibe una línea, hace su trabajo y se la pasa (o no) al siguiente. Ya está Mayusculas: escribe Numerada, SinVacias, Censura y Limite y complétalos en envolver.

  • Primera línea: filtros f1,f2,.... El primero de la lista es el que recibe cada línea primero. Después, el texto.
  • numerada antepone n: (contando las líneas que le llegan), sinvacias descarta las líneas en blanco, censura:a|b cambia cada palabra (separada por espacios) que sea a o b, sin distinguir mayúsculas, por tantos * como letras, y limite:n deja pasar solo las n primeras líneas que le llegan.
  • Un filtro desconocido escribe Filtro desconocido: f; un límite que no es un número, Límite no válido.
JavaFiltros de texto apilablesMedio

Ejemplo

Entrada (lo que se escribe por teclado)
filtros sinvacias,numerada
Primera línea

Segunda línea
   
Tercera
Salida esperada
1: Primera línea
2: Segunda línea
3: Tercera
Test oculto #3
Test oculto #4
Test oculto #5
0/5 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3/** Por donde sale el texto, línea a línea. */
4interface Salida {
5    void escribir(String linea);
6}
7
8class Consola implements Salida {
9    public void escribir(String linea) { System.out.println(linea); }
10}
11
12/** El decorador base: envuelve otra salida y le pasa las líneas. */
13abstract class Filtro implements Salida {
14    protected final Salida siguiente;
15    Filtro(Salida siguiente) { this.siguiente = siguiente; }
16}
17
18class Mayusculas extends Filtro {
19    Mayusculas(Salida s) { super(s); }
20    public void escribir(String linea) { siguiente.escribir(linea.toUpperCase()); }
21}
22
23class Numerada extends Filtro {
24    private int n = 0;
25    Numerada(Salida s) { super(s); }
26    public void escribir(String linea) { siguiente.escribir(++n + ": " + linea); }
27}
28
29class SinVacias extends Filtro {
30    SinVacias(Salida s) { super(s); }
31    public void escribir(String linea) {
32        if (!linea.isBlank()) siguiente.escribir(linea);
33    }
34}
35
36class Censura extends Filtro {
37    private final Set<String> palabras = new HashSet<>();
38    Censura(Salida s, List<String> palabras) {
39        super(s);
40        for (String p : palabras) this.palabras.add(p.toLowerCase());
41    }
42    public void escribir(String linea) {
43        String[] trozos = linea.split(" ", -1);
44        for (int i = 0; i < trozos.length; i++) {
45            if (palabras.contains(trozos[i].toLowerCase())) trozos[i] = "*".repeat(trozos[i].length());
46        }
47        siguiente.escribir(String.join(" ", trozos));
48    }
49}
50
51class Limite extends Filtro {
52    private int quedan;
53    Limite(Salida s, int maximo) { super(s); quedan = maximo; }
54    public void escribir(String linea) {
55        if (quedan > 0) {
56            quedan--;
57            siguiente.escribir(linea);
58        }
59    }
60}
61
62public class Main {
63    static Salida envolver(String filtro, Salida s) {
64        String[] p = filtro.split(":", 2);
65        return switch (p[0]) {
66            case "mayusculas" -> new Mayusculas(s);
67            case "numerada" -> new Numerada(s);
68            case "sinvacias" -> new SinVacias(s);
69            case "censura" -> new Censura(s, List.of(p.length > 1 ? p[1].split("\\|") : new String[0]));
70            case "limite" -> new Limite(s, Integer.parseInt(p.length > 1 ? p[1] : "x"));
71            default -> throw new IllegalArgumentException("Filtro desconocido: " + filtro);
72        };
73    }
74
75    public static void main(String[] args) {
76        Scanner sc = new Scanner(System.in);
77        if (!sc.hasNextLine()) return;
78        String cabecera = sc.nextLine().trim();
79        String[] filtros = cabecera.startsWith("filtros ") ? cabecera.substring(8).split(",") : new String[0];
80        Salida salida = new Consola();
81        try {
82            // El primer filtro de la lista es el que recibe cada línea primero: se envuelve el último
83            for (int i = filtros.length - 1; i >= 0; i--) salida = envolver(filtros[i].trim(), salida);
84        } catch (NumberFormatException e) {
85            System.out.println("Límite no válido");
86            return;
87        } catch (IllegalArgumentException e) {
88            System.out.println(e.getMessage());
89            return;
90        }
91        while (sc.hasNextLine()) salida.escribir(sc.nextLine());
92    }
93}

Cada filtro es pequeño y no sabe nada de los demás; la pila se monta en tiempo de ejecución a partir de la primera línea.

Es la misma idea que los middlewares de un servidor web: cada capa decide si pasa la petición a la siguiente y qué le cambia.

Test

Test: Decorator

0/5 respondidas · 0 aciertos

Elige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.

  1. 1.¿Qué tiene en común un decorador con el objeto que envuelve?

  2. 2.¿Qué problema evita frente a la herencia?

  3. 3.En new BufferedReader(new InputStreamReader(new FileInputStream(f))), ¿cuál es el objeto decorado más interno?

  4. 4.¿Importa el orden en que se apilan los decoradores?

  5. 5.¿Qué error deja sin efecto toda la pila de decoradores?

Relacionado