Apuntes DAM

Patrón Facade

Una clase que da una puerta sencilla a un subsistema complicado: matricular, comprar o encender el home cinema es una llamada, y la fachada coordina por dentro las clases que hagan falta en su orden.

nivel básicoTambién: fachada, capa de servicio, servicio

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.

Facade

Escribe las solicitudes de matrícula separadas por comas: nombre, DNI y ciclo (DAM, DAW o ASIR). Quedan 2 plazas en DAM, 1 en DAW y ninguna en ASIR.

Paso 1

La fachada tiene un método por operación (matricular) y conoce las cuatro clases del subsistema. El cliente solo la conoce a ella.

1boolean matricular(String nombre, String dni, String ciclo) {  // solicitudes = 3
2    if (!validador.dniValido(dni)) return false;
3    if (!plazas.reservar(ciclo)) return false;
4    String exp = expedientes.crear(nombre, dni, ciclo);
5    correo.enviar(nombre, "matriculado con " + exp);
6    return true;
7}

Variables

solicitudes
3

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

La idea

Matricular a un alumno exige validar su DNI, reservar plaza, crear el expediente y enviarle un correo, en ese orden y parando si algo falla. Si cada pantalla o cada controlador que matricula hace esos pasos por su cuenta, el código se repite, el orden se equivoca y todas las pantallas dependen de las cuatro clases.

El patrón Facade pone delante del subsistema una clase con métodos de alto nivel (matricular(nombre, dni, ciclo)). La fachada conoce las clases de dentro y las coordina; el resto del programa solo conoce la fachada. Las clases del subsistema siguen ahí y se pueden usar directamente si hace falta algo especial: la fachada simplifica, no esconde a la fuerza.

En las aplicaciones por capas, la capa de servicio es una fachada: el controlador llama a pedidoService.comprar(...) y no sabe nada del DAO, de la pasarela de pago ni del correo. Files.readAllLines es otra: lee un fichero entero sin que abras flujos ni búferes.

Cuándo usarlo

  • Una operación habitual necesita varias clases de un subsistema en un orden concreto.
  • Quieres que el resto del programa dependa de una sola clase y no de todas las del subsistema (para poder cambiarlas sin tocar nada más).
  • Quieres organizar una aplicación en capas: la de presentación habla con los servicios, no con el acceso a datos.

Cuándo no

  • Si la fachada acaba teniendo un método por cada método del subsistema: no simplifica nada.
  • Si se convierte en una «clase dios» que lo hace todo: mejor varias fachadas pequeñas, una por área.

Participantes

  1. Fachada. La clase con las operaciones de alto nivel (SecretariaFacade.matricular), que delega en el subsistema.
  2. Subsistema. Las clases que hacen el trabajo (Validador, Plazas, Expedientes, Correo). No saben que hay una fachada.
  3. Cliente. Usa la fachada en lugar de coordinar el subsistema por su cuenta.

Diagrama de clases

usaSecretariaFacade+matricular(nombre : String, dni : String, ciclo : String) : booleanValidador+dniValido(dni : String) : booleanPlazas+reservar(ciclo : String) : booleanExpedientes+crear(nombre : String, dni : String, ciclo : String) : StringCorreo+enviar(destino : String, texto : String) : voidCliente
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
1@startuml
2class SecretariaFacade {
3  +matricular(nombre : String, dni : String, ciclo : String) : boolean
4}
5class Validador {
6  +dniValido(dni : String) : boolean
7}
8class Plazas {
9  +reservar(ciclo : String) : boolean
10}
11class Expedientes {
12  +crear(nombre : String, dni : String, ciclo : String) : String
13}
14class Correo {
15  +enviar(destino : String, texto : String) : void
16}
17class Cliente
18SecretariaFacade --> Validador
19SecretariaFacade --> Plazas
20SecretariaFacade --> Expedientes
21SecretariaFacade --> Correo
22Cliente ..> SecretariaFacade : usa
23@enduml

Puedes copiarlo en el editor de diagramas UML y modificarlo.

El código

La secretaría de un instituto

El main hace tres matrículas con una llamada cada una; la fachada valida, reserva, crea el expediente y avisa, y se para en el primer paso que falla.

Java
1import java.util.*;
2
3// ───── El subsistema: varias clases, cada una con su trabajo ─────
4class Validador {
5    boolean dniValido(String dni) {
6        if (!dni.matches("\\d{8}[A-Z]")) return false;
7        return "TRWAGMYFPDXBNJZSQVHLCKE".charAt(Integer.parseInt(dni.substring(0, 8)) % 23) == dni.charAt(8);
8    }
9}
10
11class Plazas {
12    private final Map<String, Integer> libres = new HashMap<>(Map.of("DAM", 2, "DAW", 1));
13    boolean reservar(String ciclo) {
14        int quedan = libres.getOrDefault(ciclo, 0);
15        if (quedan == 0) return false;
16        libres.put(ciclo, quedan - 1);
17        return true;
18    }
19}
20
21class Expedientes {
22    private int siguiente = 1;
23    String crear(String nombre, String dni, String ciclo) {
24        return String.format("EXP-%03d", siguiente++);
25    }
26}
27
28class Correo {
29    void enviar(String destino, String texto) { System.out.println("  Correo a " + destino + ": " + texto); }
30}
31
32/** La fachada: una sola puerta, sencilla, para una operación que usa todo el subsistema. */
33class SecretariaFacade {
34    private final Validador validador = new Validador();
35    private final Plazas plazas = new Plazas();
36    private final Expedientes expedientes = new Expedientes();
37    private final Correo correo = new Correo();
38
39    boolean matricular(String nombre, String dni, String ciclo) {
40        if (!validador.dniValido(dni)) {
41            System.out.println("  DNI no válido: " + dni);
42            return false;
43        }
44        if (!plazas.reservar(ciclo)) {
45            System.out.println("  No quedan plazas en " + ciclo);
46            return false;
47        }
48        String expediente = expedientes.crear(nombre, dni, ciclo);
49        correo.enviar(nombre, "matriculado en " + ciclo + " con el expediente " + expediente);
50        return true;
51    }
52}
53
54public class Main {
55    public static void main(String[] args) {
56        SecretariaFacade secretaria = new SecretariaFacade();   // el cliente solo conoce la fachada
57        String[][] solicitudes = {{"Ana", "12345678Z", "DAW"}, {"Luis", "12345678A", "DAM"}, {"Marta", "00000000T", "DAW"}};
58        for (String[] s : solicitudes) {
59            System.out.println("Matrícula de " + s[0] + ":");
60            System.out.println(secretaria.matricular(s[0], s[1], s[2]) ? "  Hecha" : "  Rechazada");
61        }
62    }
63}

Salida al ejecutarlo (la misma en los 5 lenguajes)

Matrícula de Ana:
  Correo a Ana: matriculado en DAW con el expediente EXP-001
  Hecha
Matrícula de Luis:
  DNI no válido: 12345678A
  Rechazada
Matrícula de Marta:
  No quedan plazas en DAW
  Rechazada

Fachadas que ya usas

Files frente a abrir los flujos a mano, y la capa de servicio de una aplicación.

Java
1// Files es una fachada: lee un fichero entero sin abrir flujos, búferes ni decodificadores a mano
2List<String> lineas = Files.readAllLines(Path.of("notas.csv"));
3Files.writeString(Path.of("informe.txt"), "Hecho");
4
5// Lo mismo, sin la fachada:
6try (BufferedReader lector = new BufferedReader(new InputStreamReader(
7        new FileInputStream("notas.csv"), StandardCharsets.UTF_8))) {
8    List<String> todas = new ArrayList<>();
9    String l;
10    while ((l = lector.readLine()) != null) todas.add(l);
11}
12
13// En una aplicación, la capa de servicio suele ser una fachada: el controlador llama a
14// pedidoService.comprar(...) y no conoce el DAO, la pasarela de pago ni el correo.

En la práctica

  • La capa de servicio de Spring, Laravel o ASP.NET: cada servicio es una fachada sobre repositorios, correo, pagos…
  • Files.readAllLines, Files.writeString y Files.copy en Java; fetch en el navegador frente a XMLHttpRequest.
  • Las bibliotecas que envuelven una API compleja en unas pocas funciones (un SDK de pagos, JdbcTemplate sobre JDBC).
  • En Android, un repositorio que decide si los datos salen de la red o de la base de datos local es una fachada para las pantallas.

Errores típicos

  • Meter en la fachada la lógica que pertenece a las clases del subsistema: debe coordinar, no hacer su trabajo.
  • Una fachada gigante con decenas de métodos de todas las áreas: se convierte en una clase imposible de mantener.
  • No respetar el orden de los pasos ni deshacer lo hecho cuando uno falla (cobrar y no tener stock).
  • Obligar a todo el mundo a pasar por la fachada incluso para casos especiales: el subsistema sigue disponible.

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. El mando del home cinema

Ver una película exige atenuar las luces, encender el proyector y elegir su entrada, encender el amplificador y ponerle volumen, y reproducir. Escribe la fachada HomeCinema, con verPelicula, volumen y terminar, que haga cada operación con una sola llamada y en el orden indicado.

  • Órdenes: ver título, volumen n y terminar. Los aparatos ya escriben lo que hacen.
  • ver: luces al 10 %, proyector encendido, entrada HDMI1, amplificador encendido, volumen (20 al empezar, o el último que se fijó) y reproducir. Si ya hay una película: Ya se está viendo «título».
  • volumen n (de 0 a 50, si no Volumen fuera de rango: n): se guarda y, si hay película, se aplica al amplificador. terminar: parar, apagar amplificador, apagar proyector y luces al 100 %; sin película, No hay ninguna película en marcha.
JavaEl mando del home cinemaFácil

Ejemplo

Entrada (lo que se escribe por teclado)
ver Dune
volumen 30
terminar
Salida esperada
Luces al 10 %
Proyector encendido
Proyector: entrada HDMI1
Amplificador encendido
Amplificador: volumen 20
Reproduciendo «Dune»
Amplificador: volumen 30
Parado «Dune»
Amplificador apagado
Proyector apagado
Luces al 100 %
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// ───── El subsistema del home cinema ─────
4class Proyector {
5    boolean encendido;
6    void encender() { encendido = true; System.out.println("Proyector encendido"); }
7    void apagar() { encendido = false; System.out.println("Proyector apagado"); }
8    void entrada(String fuente) { System.out.println("Proyector: entrada " + fuente); }
9}
10
11class Amplificador {
12    void encender() { System.out.println("Amplificador encendido"); }
13    void apagar() { System.out.println("Amplificador apagado"); }
14    void volumen(int v) { System.out.println("Amplificador: volumen " + v); }
15}
16
17class Luces {
18    void atenuar(int porcentaje) { System.out.println("Luces al " + porcentaje + " %"); }
19}
20
21class Reproductor {
22    String titulo;
23    void reproducir(String titulo) { this.titulo = titulo; System.out.println("Reproduciendo «" + titulo + "»"); }
24    void parar() { System.out.println("Parado «" + titulo + "»"); titulo = null; }
25}
26
27/** La fachada: dos órdenes sencillas que coordinan los cuatro aparatos en el orden correcto. */
28class HomeCinema {
29    private final Proyector proyector = new Proyector();
30    private final Amplificador amplificador = new Amplificador();
31    private final Luces luces = new Luces();
32    private final Reproductor reproductor = new Reproductor();
33    private int volumen = 20;
34
35    void verPelicula(String titulo) {
36        if (reproductor.titulo != null) {
37            System.out.println("Ya se está viendo «" + reproductor.titulo + "»");
38            return;
39        }
40        luces.atenuar(10);
41        proyector.encender();
42        proyector.entrada("HDMI1");
43        amplificador.encender();
44        amplificador.volumen(volumen);
45        reproductor.reproducir(titulo);
46    }
47
48    void volumen(int v) {
49        if (v < 0 || v > 50) {
50            System.out.println("Volumen fuera de rango: " + v);
51            return;
52        }
53        volumen = v;
54        if (reproductor.titulo != null) amplificador.volumen(v);
55    }
56
57    void terminar() {
58        if (reproductor.titulo == null) {
59            System.out.println("No hay ninguna película en marcha");
60            return;
61        }
62        reproductor.parar();
63        amplificador.apagar();
64        proyector.apagar();
65        luces.atenuar(100);
66    }
67}
68
69public class Main {
70    public static void main(String[] args) {
71        HomeCinema cine = new HomeCinema();
72        Scanner sc = new Scanner(System.in);
73        while (sc.hasNextLine()) {
74            String linea = sc.nextLine().trim();
75            if (linea.isEmpty()) continue;
76            String[] p = linea.split("\\s+", 2);
77            try {
78                switch (p[0]) {
79                    case "ver" -> cine.verPelicula(p[1]);
80                    case "volumen" -> cine.volumen(Integer.parseInt(p[1]));
81                    case "terminar" -> cine.terminar();
82                    default -> System.out.println("Orden no válida: " + linea);
83                }
84            } catch (RuntimeException e) {
85                System.out.println("Orden no válida: " + linea);
86            }
87        }
88    }
89}

El main da órdenes de alto nivel; las seis llamadas a los aparatos, y su orden, están en un solo sitio.

Si se cambia el proyector por otro modelo, solo se toca la fachada.

2. Comprar en la tienda online

Comprar toca cuatro subsistemas: inventario, envíos, pasarela de pago y facturas. Escribe la fachada Tienda con comprar y stock, que coordine los pasos en el orden correcto y sin dejar nada a medias: si el pago se rechaza, el stock no debe cambiar.

  • Órdenes: comprar producto cantidad tarjeta provincia y stock producto. Productos: portatil, raton y monitor.
  • Comprobaciones, en orden: Producto desconocido: x, Cantidad no válida: n (menor que 1), Sin stock suficiente de x (quedan n) y, tras calcular el total (importe + envío), Pago rechazado: total.
  • Si todo va bien: se retira el stock y Factura F-0001: n × producto importe + envío E = total, con los importes en euros con coma. stock escribe Stock de x: n.
JavaComprar en la tienda onlineMedio

Ejemplo

Entrada (lo que se escribe por teclado)
comprar raton 2 VISA1 Madrid
comprar portatil 1 VISA1 Canarias
comprar raton 1 VISA2 Baleares
stock raton
stock portatil
Salida esperada
Factura F-0001: 2 × raton 39,80 € + envío 4,95 € = 44,75 €
Factura F-0002: 1 × portatil 799,00 € + envío 0,00 € = 799,00 €
Factura F-0003: 1 × raton 19,90 € + envío 15,00 € = 34,90 €
Stock de raton: 7
Stock de portatil: 2
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// ───── El subsistema de la tienda ─────
4class Inventario {
5    private final Map<String, Integer> stock = new HashMap<>(Map.of("portatil", 3, "raton", 10, "monitor", 1));
6    private final Map<String, Double> precios = Map.of("portatil", 799.0, "raton", 19.9, "monitor", 189.0);
7    boolean existe(String producto) { return precios.containsKey(producto); }
8    double precio(String producto) { return precios.get(producto); }
9    boolean hay(String producto, int cantidad) { return stock.get(producto) >= cantidad; }
10    void retirar(String producto, int cantidad) { stock.merge(producto, -cantidad, Integer::sum); }
11    int stock(String producto) { return stock.get(producto); }
12}
13
14class Envios {
15    double coste(String provincia, double importe) {
16        if (importe >= 100) return 0;
17        return provincia.equals("Canarias") || provincia.equals("Baleares") ? 15 : 4.95;
18    }
19}
20
21class Pasarela {
22    private final Map<String, Double> saldo = new HashMap<>(Map.of("VISA1", 2000.0, "VISA2", 50.0));
23    boolean cobrar(String tarjeta, double importe) {
24        double s = saldo.getOrDefault(tarjeta, -1.0);
25        if (s < importe) return false;
26        saldo.put(tarjeta, s - importe);
27        return true;
28    }
29}
30
31class Facturas {
32    private int n = 0;
33    String emitir() { return String.format("F-%04d", ++n); }
34}
35
36/** La fachada: comprar es una sola llamada, aunque toque cuatro subsistemas en un orden concreto. */
37class Tienda {
38    private final Inventario inventario = new Inventario();
39    private final Envios envios = new Envios();
40    private final Pasarela pasarela = new Pasarela();
41    private final Facturas facturas = new Facturas();
42
43    String comprar(String producto, int cantidad, String tarjeta, String provincia) {
44        if (!inventario.existe(producto)) return "Producto desconocido: " + producto;
45        if (cantidad < 1) return "Cantidad no válida: " + cantidad;
46        if (!inventario.hay(producto, cantidad)) return "Sin stock suficiente de " + producto + " (quedan " + inventario.stock(producto) + ")";
47        double importe = inventario.precio(producto) * cantidad;
48        double envio = envios.coste(provincia, importe);
49        double total = importe + envio;
50        if (!pasarela.cobrar(tarjeta, total)) return "Pago rechazado: " + Main.euros(total);   // no se ha tocado el stock
51        inventario.retirar(producto, cantidad);
52        return "Factura " + facturas.emitir() + ": " + cantidad + " × " + producto + " " + Main.euros(importe)
53                + " + envío " + Main.euros(envio) + " = " + Main.euros(total);
54    }
55
56    String stock(String producto) {
57        return inventario.existe(producto) ? "Stock de " + producto + ": " + inventario.stock(producto) : "Producto desconocido: " + producto;
58    }
59}
60
61public class Main {
62    static String euros(double x) {
63        return String.format(Locale.ROOT, "%.2f", x).replace('.', ',') + " €";
64    }
65
66    public static void main(String[] args) {
67        Tienda tienda = new Tienda();
68        Scanner sc = new Scanner(System.in);
69        while (sc.hasNextLine()) {
70            String linea = sc.nextLine().trim();
71            if (linea.isEmpty()) continue;
72            String[] p = linea.split("\\s+");
73            try {
74                if (p[0].equals("comprar") && p.length == 5) System.out.println(tienda.comprar(p[1], Integer.parseInt(p[2]), p[3], p[4]));
75                else if (p[0].equals("stock") && p.length == 2) System.out.println(tienda.stock(p[1]));
76                else System.out.println("Orden no válida: " + linea);
77            } catch (NumberFormatException e) {
78                System.out.println("Orden no válida: " + linea);
79            }
80        }
81    }
82}

El orden de los pasos es una decisión de negocio que queda escrita en un solo sitio, la fachada, en lugar de repetirse en cada pantalla que vende.

En una aplicación real, esta clase sería el servicio de pedidos, y el controlador web solo llamaría a comprar.

Test

Test: Facade

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é hace una fachada?

  2. 2.¿Pueden usarse las clases del subsistema sin pasar por la fachada?

  3. 3.En una aplicación por capas, ¿qué capa suele hacer de fachada?

  4. 4.¿Qué riesgo tiene una fachada?

  5. 5.¿En qué se diferencia de un Adapter?

Relacionado