Apuntes DAM

Patrón Chain of Responsibility

Pasa una petición por una cadena de objetos hasta que uno la atiende: aprobaciones por niveles, soporte técnico, filtros web o middlewares. Cada eslabón la resuelve o la pasa al siguiente.

nivel intermedioTambién: cadena de responsabilidad, cadena de manejadores, middleware, filtros

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.

Chain of Responsibility

Escribe los gastos separados por comas, con su concepto y su importe. Cada uno entra por el primer eslabón de la cadena.

Paso 1

Gasto de 120,00 € (material): el cliente solo conoce el primer eslabón y se lo da a él.

1void aprobar(Gasto g) {  // gasto = material 120,00 €
2    if (puedeAprobar(g))
3        System.out.println("Lo aprueba " + nombre());
4    else if (siguiente != null)
5        siguiente.aprobar(g);
6    else
7        System.out.println("Nadie lo aprueba");
8}

Variables

gasto
material 120,00 €

Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.

La idea

Un gasto de empresa lo aprueba la jefa de equipo si es pequeño, el director si es mediano y el consejo si es grande. Si el código que pide la aprobación tiene que saber a quién mandar cada gasto, se llena de if con las reglas de todos los niveles, y cambiar un límite o añadir un nivel obliga a tocarlo.

El patrón Chain of Responsibility encadena los posibles responsables. Cada eslabón tiene una referencia al siguiente y, al recibir una petición, decide: la atiende o se la pasa. El que pide solo conoce el primer eslabón y no sabe quién acabará atendiéndola. Cambiar el orden, añadir o quitar niveles es cambiar cómo se enlaza la cadena.

Hay dos variantes. En la clásica, la petición avanza hasta que alguien la atiende (aprobaciones, soporte). En la de filtros o middlewares, cada eslabón hace su parte y decide si deja seguir a la siguiente (comprobar la sesión, registrar, comprimir) o corta la cadena. Los filtros de los servlets, los middlewares de Express, Laravel y ASP.NET y la propagación de excepciones funcionan así.

Cuándo usarlo

  • Varios objetos pueden atender una petición y no se sabe de antemano cuál lo hará.
  • El que pide no debe conocer a todos los posibles responsables ni sus reglas.
  • Los responsables o su orden cambian con la configuración (niveles de soporte, filtros activados).
  • Una petición tiene que pasar por varios pasos que pueden cortarla: autenticación, permisos, validación, registro.

Cuándo no

  • Si siempre la atiende el mismo objeto: una llamada directa es más clara.
  • Si es importante garantizar que alguien la atiende: con una cadena mal configurada, la petición puede llegar al final sin respuesta.

Participantes

  1. Manejador. La clase base (Aprobador) con la referencia al siguiente y el método que atiende o pasa la petición.
  2. Manejadores concretos. Cada eslabón (JefeEquipo, Director, Consejo) decide si puede atender.
  3. Cliente. Monta la cadena (o la recibe ya montada) y envía cada petición al primer eslabón.

Diagrama de clases

0..1siguienteaprueba«abstract»Aprobador-siguiente : Aprobador+luego(siguiente : Aprobador) : Aprobador+aprobar(g : Gasto) : void#nombre() : String#puedeAprobar(g : Gasto) : booleanJefeEquipoDirectorConsejoGasto+concepto : String+importe : double
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
1@startuml
2abstract class Aprobador {
3  -siguiente : Aprobador
4  +luego(siguiente : Aprobador) : Aprobador
5  +aprobar(g : Gasto) : void
6  #{abstract} nombre() : String
7  #{abstract} puedeAprobar(g : Gasto) : boolean
8}
9class JefeEquipo
10class Director
11class Consejo
12class Gasto {
13  +concepto : String
14  +importe : double
15}
16Aprobador <|-- JefeEquipo
17Aprobador <|-- Director
18Aprobador <|-- Consejo
19Aprobador --> "0..1" Aprobador : siguiente
20Aprobador ..> Gasto : aprueba
21@enduml

Puedes copiarlo en el editor de diagramas UML y modificarlo.

El código

Aprobar gastos por niveles

El main solo llama al primer eslabón. Cada nivel aprueba si puede o pasa el gasto, y el último avisa si nadie puede.

Java
1import java.util.*;
2
3record Gasto(String concepto, double importe) {}
4
5/** Un eslabón de la cadena: aprueba lo que puede y pasa el resto al siguiente. */
6abstract class Aprobador {
7    private Aprobador siguiente;
8
9    /** Enlaza el siguiente y lo devuelve, para escribir la cadena de un tirón. */
10    Aprobador luego(Aprobador siguiente) {
11        this.siguiente = siguiente;
12        return siguiente;
13    }
14
15    void aprobar(Gasto g) {
16        if (puedeAprobar(g)) {
17            System.out.println("  Lo aprueba " + nombre());
18        } else if (siguiente != null) {
19            System.out.println("  " + nombre() + " no puede: lo pasa");
20            siguiente.aprobar(g);
21        } else {
22            System.out.println("  " + nombre() + " tampoco puede: nadie lo aprueba");
23        }
24    }
25
26    protected abstract String nombre();
27    protected abstract boolean puedeAprobar(Gasto g);
28}
29
30class JefeEquipo extends Aprobador {
31    protected String nombre() { return "la jefa de equipo"; }
32    protected boolean puedeAprobar(Gasto g) { return g.importe() <= 500 && !g.concepto().equals("viaje"); }
33}
34
35class Director extends Aprobador {
36    protected String nombre() { return "el director"; }
37    protected boolean puedeAprobar(Gasto g) { return g.importe() <= 5000; }
38}
39
40class Consejo extends Aprobador {
41    protected String nombre() { return "el consejo"; }
42    protected boolean puedeAprobar(Gasto g) { return g.importe() <= 50000; }
43}
44
45public class Main {
46    public static void main(String[] args) {
47        Aprobador cadena = new JefeEquipo();
48        cadena.luego(new Director()).luego(new Consejo());
49        List<Gasto> gastos = List.of(new Gasto("material", 120), new Gasto("viaje", 300), new Gasto("servidor", 12000), new Gasto("edificio", 900000));
50        for (Gasto g : gastos) {
51            System.out.println("Gasto de " + String.format(Locale.ROOT, "%.2f", g.importe()).replace('.', ',') + " € (" + g.concepto() + "):");
52            cadena.aprobar(g);                       // el cliente solo conoce el primer eslabón
53        }
54    }
55}

Salida al ejecutarlo (la misma en los 5 lenguajes)

Gasto de 120,00 € (material):
  Lo aprueba la jefa de equipo
Gasto de 300,00 € (viaje):
  la jefa de equipo no puede: lo pasa
  Lo aprueba el director
Gasto de 12000,00 € (servidor):
  la jefa de equipo no puede: lo pasa
  el director no puede: lo pasa
  Lo aprueba el consejo
Gasto de 900000,00 € (edificio):
  la jefa de equipo no puede: lo pasa
  el director no puede: lo pasa
  el consejo tampoco puede: nadie lo aprueba

Filtros de servlets y excepciones

La variante de filtros: cada eslabón decide si deja seguir a la petición.

Java
1// Los filtros de un servidor web son una cadena: cada uno hace lo suyo y decide si sigue
2@WebFilter("/*")
3public class FiltroLogin implements Filter {
4    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain cadena)
5            throws IOException, ServletException {
6        if (((HttpServletRequest) req).getSession().getAttribute("usuario") == null) {
7            ((HttpServletResponse) resp).sendRedirect("/login");    // corta la cadena aquí
8            return;
9        }
10        cadena.doFilter(req, resp);                                   // pasa al siguiente filtro (o al servlet)
11    }
12}
13
14// Las excepciones recorren otra cadena: si un método no la captura, sube al que lo llamó,
15// y así hasta que alguien la atiende (o hasta el main).

En la práctica

  • Los filtros de los servlets (FilterChain.doFilter) y los de Spring Security.
  • Los middlewares de Express (next()), Laravel ($next($request)) y ASP.NET Core (await next()).
  • La propagación de excepciones: si un método no la captura, sube al que lo llamó.
  • Los eventos del DOM que suben de un elemento a sus padres (burbujeo) hasta que alguien los atiende o los para.

Errores típicos

  • Olvidar pasar la petición al siguiente cuando un eslabón no la atiende: la cadena se corta sin querer.
  • No contemplar el final de la cadena: una petición que nadie atiende debe tener una respuesta clara.
  • Ordenar mal los eslabones: si el más general va primero, los demás nunca reciben nada.
  • Crear ciclos en la cadena (un eslabón que apunta a uno anterior): la petición da vueltas para siempre.

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. Filtros de comentarios

Antes de publicar un comentario, pasa por una cadena de filtros. Ya está el de longitud. Escribe los filtros Enlaces, Prohibidas y Mayusculas y enlázalos detrás, en ese orden: el primero que encuentra un problema da el motivo del rechazo.

  • La primera línea es prohibidas palabra,palabra; después, un comentario por línea. Salida: Publicado: texto o Rechazado (motivo): texto (un comentario vacío es solo Rechazado (está vacío)), y al final N publicados, M rechazados.
  • Motivos: lleva enlaces (contiene http://, https:// o www., sin distinguir mayúsculas), contiene «palabra» (una palabra prohibida, sin distinguir mayúsculas; las palabras se separan por todo lo que no son letras) y está escrito en mayúsculas (al menos 10 letras y más de la mitad en mayúscula).
JavaFiltros de comentariosFácil

Ejemplo

Entrada (lo que se escribe por teclado)
prohibidas tonto,feo
Me ha encantado el tema de herencia
Mira mi web: www.ejemplo.com
Eres un TONTO
ESTO ES UNA VERGÜENZA
OK
Salida esperada
Publicado: Me ha encantado el tema de herencia
Rechazado (lleva enlaces): Mira mi web: www.ejemplo.com
Rechazado (contiene «tonto»): Eres un TONTO
Rechazado (está escrito en mayúsculas): ESTO ES UNA VERGÜENZA
Publicado: OK
2 publicados, 3 rechazados
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
3/** Un filtro de la cadena: si encuentra un problema, lo dice; si no, pasa el texto al siguiente. */
4abstract class Filtro {
5    private Filtro siguiente;
6
7    Filtro luego(Filtro siguiente) {
8        this.siguiente = siguiente;
9        return siguiente;
10    }
11
12    /** null si el comentario pasa toda la cadena desde aquí; si no, el motivo del primer rechazo. */
13    final String revisar(String texto) {
14        String motivo = problema(texto);
15        if (motivo != null) return motivo;
16        return siguiente == null ? null : siguiente.revisar(texto);
17    }
18
19    protected abstract String problema(String texto);
20}
21
22class Longitud extends Filtro {
23    protected String problema(String texto) {
24        if (texto.isBlank()) return "está vacío";
25        return texto.length() > 120 ? "es demasiado largo" : null;
26    }
27}
28
29class Enlaces extends Filtro {
30    protected String problema(String texto) {
31        String t = texto.toLowerCase();
32        return t.contains("http://") || t.contains("https://") || t.contains("www.") ? "lleva enlaces" : null;
33    }
34}
35
36class Prohibidas extends Filtro {
37    private final Set<String> palabras;
38    Prohibidas(Set<String> palabras) { this.palabras = palabras; }
39    protected String problema(String texto) {
40        for (String w : texto.toLowerCase().split("[^\\p{L}]+")) {
41            if (palabras.contains(w)) return "contiene «" + w + "»";
42        }
43        return null;
44    }
45}
46
47class Mayusculas extends Filtro {
48    protected String problema(String texto) {
49        int letras = 0, mayusculas = 0;
50        for (char c : texto.toCharArray()) {
51            if (Character.isLetter(c)) {
52                letras++;
53                if (Character.isUpperCase(c)) mayusculas++;
54            }
55        }
56        return letras >= 10 && mayusculas * 2 > letras ? "está escrito en mayúsculas" : null;
57    }
58}
59
60public class Main {
61    public static void main(String[] args) {
62        Scanner sc = new Scanner(System.in);
63        Set<String> prohibidas = new HashSet<>();
64        String primera = sc.hasNextLine() ? sc.nextLine().trim() : "";
65        if (primera.startsWith("prohibidas ")) {
66            for (String w : primera.substring(11).split(",")) if (!w.isBlank()) prohibidas.add(w.trim().toLowerCase());
67        }
68        // La cadena: el orden decide qué motivo se da cuando hay varios problemas
69        Filtro cadena = new Longitud();
70        cadena.luego(new Enlaces()).luego(new Prohibidas(prohibidas)).luego(new Mayusculas());
71        int publicados = 0, rechazados = 0;
72        while (sc.hasNextLine()) {
73            String texto = sc.nextLine();
74            String motivo = cadena.revisar(texto);
75            if (motivo == null) {
76                publicados++;
77                System.out.println("Publicado: " + texto.trim());
78            } else {
79                rechazados++;
80                System.out.println("Rechazado (" + motivo + ")" + (texto.isBlank() ? "" : ": " + texto.trim()));
81            }
82        }
83        System.out.println(publicados + " publicados, " + rechazados + " rechazados");
84    }
85}

Cada filtro es independiente y no sabe qué otros hay; el orden de la cadena decide qué motivo se da primero.

Añadir un filtro nuevo es una clase y un luego más: revisar no cambia.

2. Soporte técnico por niveles

Los tickets de soporte pasan por niveles configurables. Manejador.atender atiende el ticket si el nivel puede y, si no, debería pasárselo al siguiente, pero de momento no lo hace. Complétalo, escribe los niveles Experto y Gerencia y añádelos en crear.

  • La primera línea es cadena nivel,nivel,... (el orden de la cadena, con bot, tecnico, experto y gerencia); después, tickets ticket prioridad normal|vip texto, con prioridad del 1 al 5.
  • Bot: atiende si el texto contiene contraseña o wifi. Técnico: prioridad 1 o 2. Experto: prioridad hasta 4. Gerencia: los clientes vip.
  • Salida: #n (prioridad p) → nivel o sin atender, y al final Atendidos: nivel n · ... · sin atender n. Un nivel desconocido en la cadena: Nivel desconocido: x.
JavaSoporte técnico por nivelesMedio

Ejemplo

Entrada (lo que se escribe por teclado)
cadena bot,tecnico,experto,gerencia
ticket 1 normal No me funciona la wifi
ticket 2 normal El ratón no va
ticket 4 normal Se ha caído el servidor
ticket 5 vip Necesito un informe urgente
ticket 5 normal Quiero hablar con el director
Salida esperada
#1 (prioridad 1) → bot
#2 (prioridad 2) → técnico
#3 (prioridad 4) → experto
#4 (prioridad 5) → gerencia
#5 (prioridad 5) → sin atender
Atendidos: bot 1 · técnico 1 · experto 1 · gerencia 1 · sin atender 1
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
3record Ticket(int id, int prioridad, boolean vip, String texto) {}
4
5/** Un nivel de soporte: atiende el ticket si puede y, si no, lo pasa al siguiente nivel. */
6abstract class Manejador {
7    private Manejador siguiente;
8    int atendidos = 0;
9
10    void setSiguiente(Manejador siguiente) { this.siguiente = siguiente; }
11
12    /** El nombre de quien lo atiende, o null si nadie de aquí en adelante puede. */
13    final String atender(Ticket t) {
14        if (puede(t)) {
15            atendidos++;
16            return nombre();
17        }
18        return siguiente == null ? null : siguiente.atender(t);
19    }
20
21    abstract String nombre();
22    protected abstract boolean puede(Ticket t);
23}
24
25class Bot extends Manejador {
26    String nombre() { return "bot"; }
27    protected boolean puede(Ticket t) {
28        String s = t.texto().toLowerCase();
29        return s.contains("contraseña") || s.contains("wifi");
30    }
31}
32
33class Tecnico extends Manejador {
34    String nombre() { return "técnico"; }
35    protected boolean puede(Ticket t) { return t.prioridad() <= 2; }
36}
37
38class Experto extends Manejador {
39    String nombre() { return "experto"; }
40    protected boolean puede(Ticket t) { return t.prioridad() <= 4; }
41}
42
43class Gerencia extends Manejador {
44    String nombre() { return "gerencia"; }
45    protected boolean puede(Ticket t) { return t.vip(); }
46}
47
48public class Main {
49    static Manejador crear(String nombre) {
50        return switch (nombre) {
51            case "bot" -> new Bot();
52            case "tecnico" -> new Tecnico();
53            case "experto" -> new Experto();
54            case "gerencia" -> new Gerencia();
55            default -> throw new IllegalArgumentException("Nivel desconocido: " + nombre);
56        };
57    }
58
59    public static void main(String[] args) {
60        Scanner sc = new Scanner(System.in);
61        String primera = sc.hasNextLine() ? sc.nextLine().trim() : "";
62        if (!primera.startsWith("cadena ")) {
63            System.out.println("Falta la cadena");
64            return;
65        }
66        List<Manejador> niveles = new ArrayList<>();
67        try {
68            for (String n : primera.substring(7).split(",")) niveles.add(crear(n.trim()));
69        } catch (IllegalArgumentException e) {
70            System.out.println(e.getMessage());
71            return;
72        }
73        for (int i = 0; i + 1 < niveles.size(); i++) niveles.get(i).setSiguiente(niveles.get(i + 1));
74        int id = 0, sinAtender = 0;
75        while (sc.hasNextLine()) {
76            String[] p = sc.nextLine().trim().split("\\s+", 4);
77            if (p[0].isEmpty()) continue;
78            try {
79                if (!p[0].equals("ticket") || p.length < 4 || !p[2].matches("normal|vip")) throw new NumberFormatException();
80                int prioridad = Integer.parseInt(p[1]);
81                if (prioridad < 1 || prioridad > 5) throw new NumberFormatException();
82                Ticket t = new Ticket(++id, prioridad, p[2].equals("vip"), p[3]);
83                String quien = niveles.get(0).atender(t);
84                if (quien == null) sinAtender++;
85                System.out.println("#" + t.id() + " (prioridad " + prioridad + ") → " + (quien == null ? "sin atender" : quien));
86            } catch (NumberFormatException e) {
87                System.out.println("Ticket no válido: " + String.join(" ", p));
88            }
89        }
90        List<String> resumen = new ArrayList<>();
91        for (Manejador m : niveles) resumen.add(m.nombre() + " " + m.atendidos);
92        resumen.add("sin atender " + sinAtender);
93        System.out.println("Atendidos: " + String.join(" · ", resumen));
94    }
95}

La cadena se monta a partir de la configuración: el código que reparte los tickets no cambia si se añade o se reordena un nivel.

Si el más general (experto) va antes que el técnico, el técnico no recibe nunca nada: el orden importa.

Test

Test: Chain of Responsibility

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.En Chain of Responsibility, ¿a quién envía el cliente la petición?

  2. 2.¿Qué hace un eslabón que no puede atender la petición?

  3. 3.¿Qué pasa si el manejador más general va el primero?

  4. 4.¿Cuál de estos sigue el patrón?

  5. 5.¿Qué hay que prever al final de la cadena?

Relacionado