Apuntes DAM

Patrón Abstract Factory

Una interfaz para crear familias de objetos que tienen que ir juntos (los controles de un tema, las piezas de un driver de base de datos) sin decir sus clases concretas ni poder mezclar dos familias.

nivel intermedioTambién: fábrica abstracta, kit, factoría abstracta

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.

Abstract Factory

Elige el tema y las piezas del formulario, en orden: todas salen de la misma fábrica.

casilla o boton

Paso 1

La familia se elige una sola vez, al crear la fábrica: TemaOscuro. El otro tema queda apagado: el formulario no puede sacar piezas de él.

1FabricaUI fabrica = new TemaOscuro();  // fábrica = TemaOscuro, piezas = 0
2Formulario formulario = new Formulario(fabrica);
3
4class Formulario {
5    Formulario(FabricaUI fabrica) {
6        casilla = fabrica.crearCasilla();
7        boton = fabrica.crearBoton();
8    }
9    void mostrar() {
10        System.out.println(boton.pintar("Enviar"));
11    }
12}
13
14class TemaOscuro implements FabricaUI {
15    public Boton crearBoton() { return new BotonOscuro(); }
16    public Casilla crearCasilla() { return new CasillaOscura(); }
17}

Variables

fábrica
TemaOscuro
piezas
0

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

La idea

A veces no se crea un objeto suelto sino varios que tienen que encajar: el botón, la casilla y el menú de un mismo tema visual; la conexión, la sentencia y el resultado de una misma base de datos. Si cada uno se crea con su new por separado, es fácil acabar con un botón oscuro en un formulario claro.

El patrón Abstract Factory define una interfaz (la fábrica abstracta, FabricaUI) con un método fábrica por cada producto de la familia (crearBoton, crearCasilla). Cada familia es una clase que la implementa (TemaClaro, TemaOscuro) y crea sus productos concretos. El cliente recibe una fábrica y le pide las piezas: todas salen de la misma familia.

Cambiar de familia entera es cambiar un objeto, la fábrica, en un solo sitio del programa (normalmente al arrancar, según la configuración). Añadir una familia nueva es añadir clases. Lo que cuesta es añadir un producto nuevo a todas las familias: hay que tocar la interfaz y todas las fábricas.

Cuándo usarlo

  • Hay varias familias de objetos relacionados y un programa tiene que usar los de una sola familia a la vez (temas, plataformas, bases de datos, niveles de dificultad).
  • Se quiere poder cambiar de familia en un solo sitio, sin tocar el código que usa los objetos.
  • Se quiere garantizar que nunca se mezclan piezas de familias distintas.

Cuándo no

  • Si solo se crea un tipo de objeto: basta un Factory Method.
  • Si los productos de la familia cambian a menudo: cada producto nuevo obliga a tocar todas las fábricas.

Participantes

  1. Productos abstractos. Una interfaz por cada pieza de la familia (Boton, Casilla).
  2. Productos concretos. Una clase por pieza y familia (BotonClaro, BotonOscuro, CasillaClara…).
  3. Fábrica abstracta. La interfaz con un método fábrica por pieza (FabricaUI con crearBoton y crearCasilla).
  4. Fábricas concretas. Una por familia (TemaClaro, TemaOscuro): cada método crea la pieza de su familia.
  5. Cliente. Recibe una fábrica (Formulario) y solo usa las interfaces: no sabe qué familia es.

Diagrama de clases

creacreacreacreausa«interface»FabricaUI+crearBoton() : Boton+crearCasilla() : CasillaTemaClaroTemaOscuro«interface»Boton+pintar(texto : String) : String«interface»Casilla+pintar(texto : String, marcada : boolean) : StringBotonClaroBotonOscuroCasillaClaraCasillaOscuraFormulario-boton : Boton-casilla : Casilla+mostrar() : void
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
1@startuml
2interface FabricaUI {
3  +crearBoton() : Boton
4  +crearCasilla() : Casilla
5}
6class TemaClaro
7class TemaOscuro
8interface Boton {
9  +pintar(texto : String) : String
10}
11interface Casilla {
12  +pintar(texto : String, marcada : boolean) : String
13}
14class BotonClaro
15class BotonOscuro
16class CasillaClara
17class CasillaOscura
18class Formulario {
19  -boton : Boton
20  -casilla : Casilla
21  +mostrar() : void
22}
23FabricaUI <|.. TemaClaro
24FabricaUI <|.. TemaOscuro
25Boton <|.. BotonClaro
26Boton <|.. BotonOscuro
27Casilla <|.. CasillaClara
28Casilla <|.. CasillaOscura
29TemaClaro ..> BotonClaro : crea
30TemaClaro ..> CasillaClara : crea
31TemaOscuro ..> BotonOscuro : crea
32TemaOscuro ..> CasillaOscura : crea
33Formulario ..> FabricaUI : usa
34@enduml

Puedes copiarlo en el editor de diagramas UML y modificarlo.

El código

Controles de dos temas

Formulario funciona igual con los dos temas, y no puede mezclar un botón de uno con una casilla del otro.

Java
1import java.util.*;
2
3// Los productos: cada uno tiene una versión por tema
4interface Boton { String pintar(String texto); }
5interface Casilla { String pintar(String texto, boolean marcada); }
6
7class BotonClaro implements Boton {
8    public String pintar(String texto) { return "[ " + texto + " ]"; }
9}
10class CasillaClara implements Casilla {
11    public String pintar(String texto, boolean marcada) { return (marcada ? "[x] " : "[ ] ") + texto; }
12}
13class BotonOscuro implements Boton {
14    public String pintar(String texto) { return "<< " + texto.toUpperCase() + " >>"; }
15}
16class CasillaOscura implements Casilla {
17    public String pintar(String texto, boolean marcada) { return (marcada ? "(*) " : "( ) ") + texto; }
18}
19
20/** La fábrica abstracta: crea una familia entera de productos que encajan entre sí. */
21interface FabricaUI {
22    Boton crearBoton();
23    Casilla crearCasilla();
24}
25
26class TemaClaro implements FabricaUI {
27    public Boton crearBoton() { return new BotonClaro(); }
28    public Casilla crearCasilla() { return new CasillaClara(); }
29}
30
31class TemaOscuro implements FabricaUI {
32    public Boton crearBoton() { return new BotonOscuro(); }
33    public Casilla crearCasilla() { return new CasillaOscura(); }
34}
35
36/** El cliente: monta el formulario sin saber de qué tema son las piezas, y nunca mezcla dos temas. */
37class Formulario {
38    private final Boton boton;
39    private final Casilla casilla;
40
41    Formulario(FabricaUI fabrica) {
42        boton = fabrica.crearBoton();
43        casilla = fabrica.crearCasilla();
44    }
45
46    void mostrar() {
47        System.out.println(casilla.pintar("Acepto las condiciones", true));
48        System.out.println(casilla.pintar("Quiero recibir novedades", false));
49        System.out.println(boton.pintar("Enviar"));
50    }
51}
52
53public class Main {
54    public static void main(String[] args) {
55        Map<String, FabricaUI> temas = new LinkedHashMap<>();
56        temas.put("claro", new TemaClaro());
57        temas.put("oscuro", new TemaOscuro());
58        for (var tema : temas.entrySet()) {
59            System.out.println("Tema " + tema.getKey() + ":");
60            new Formulario(tema.getValue()).mostrar();
61        }
62    }
63}

Salida al ejecutarlo (la misma en los 5 lenguajes)

Tema claro:
[x] Acepto las condiciones
[ ] Quiero recibir novedades
[ Enviar ]
Tema oscuro:
(*) Acepto las condiciones
( ) Quiero recibir novedades
<< ENVIAR >>

Elegir la familia al arrancar (y JDBC)

La fábrica se decide en un solo sitio. JDBC es una fábrica abstracta que ya usas: el driver crea todas las piezas de su base de datos.

Java
1// La familia se elige una vez, al arrancar; el resto del programa solo ve FabricaUI.
2FabricaUI fabrica = switch (config.getProperty("tema", "claro")) {
3    case "oscuro" -> new TemaOscuro();
4    default -> new TemaClaro();
5};
6Formulario formulario = new Formulario(fabrica);
7
8// JDBC funciona igual: el driver es una fábrica de una familia (conexión, sentencia, resultado).
9// Con la URL de MySQL todas las piezas son de MySQL; con la de PostgreSQL, todas de PostgreSQL,
10// y este código no cambia.
11try (Connection con = DriverManager.getConnection(url, usuario, clave);
12     PreparedStatement ps = con.prepareStatement("SELECT nombre FROM alumno WHERE ciclo = ?")) {
13    ps.setString(1, "DAM");
14    try (ResultSet rs = ps.executeQuery()) {
15        while (rs.next()) System.out.println(rs.getString("nombre"));
16    }
17}

En la práctica

  • JDBC: DriverManager da una Connection del driver de la URL, y esa conexión crea PreparedStatement y ResultSet del mismo driver. En .NET, DbProviderFactory hace exactamente esto.
  • DocumentBuilderFactory y TransformerFactory de XML: según la implementación instalada, crean analizadores y transformadores que trabajan juntos.
  • Los temas de una interfaz (claro, oscuro, alto contraste) y las bibliotecas multiplataforma que crean controles de Windows, macOS o Linux.
  • Los juegos con niveles de dificultad: la fábrica del modo difícil crea enemigos, armas y obstáculos de ese modo.

Errores típicos

  • Crear las piezas de la familia por separado (new BotonOscuro() junto a new CasillaClara()): se pierde la garantía de que encajan.
  • Elegir la familia con un if en cada sitio que crea una pieza: la elección debe hacerse una vez, al crear la fábrica.
  • Confundirlo con Factory Method: aquí la fábrica es un objeto con un método por cada producto de una familia; allí, un método que crea un solo tipo de producto.
  • Meter en la fábrica abstracta productos que solo usa una familia: obliga a las demás a implementar métodos que no tienen sentido para ellas.

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. Muebles de una misma colección

La tienda vende muebles por estilos, y un pedido nunca mezcla estilos: el main pide cada pieza a la fábrica del estilo elegido. Ya está el estilo moderno. Escribe las familias Clasico y Nordico, con sus tres muebles, y regístralas en el main con las claves clasico y nordico.

  • Órdenes: estilo nombre (escribe Estilo: nombre, o Estilo desconocido: nombre y se queda el anterior) y pedido pieza pieza… con silla, mesa o sofa: una línea por pieza Descripción: precio € y Total: X €.
  • Clásico: «Silla clásica de madera tallada» 60 €, «Mesa clásica de roble» 210 €, «Sofá clásico de terciopelo» 520 €; su estilo() es clásico.
  • Nórdico: «Silla nórdica de pino» 39 €, «Mesa nórdica de abedul» 99 €, «Sofá nórdico de lino» 299 €; su estilo() es nórdico.
JavaMuebles de una misma colecciónFácil

Ejemplo

Entrada (lo que se escribe por teclado)
estilo nordico
pedido silla silla mesa
estilo clasico
pedido sofa mesa
estilo moderno
pedido sofa
Salida esperada
Estilo: nórdico
  Silla nórdica de pino: 39,00 €
  Silla nórdica de pino: 39,00 €
  Mesa nórdica de abedul: 99,00 €
Total: 177,00 €
Estilo: clásico
  Sofá clásico de terciopelo: 520,00 €
  Mesa clásica de roble: 210,00 €
Total: 730,00 €
Estilo: moderno
  Sofá moderno de piel: 380,00 €
Total: 380,00 €
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 Silla { String describir(); double precio(); }
4interface Mesa { String describir(); double precio(); }
5interface Sofa { String describir(); double precio(); }
6
7/** La fábrica abstracta: los muebles de un mismo estilo. */
8interface FabricaMuebles {
9    String estilo();
10    Silla crearSilla();
11    Mesa crearMesa();
12    Sofa crearSofa();
13}
14
15class SillaModerna implements Silla {
16    public String describir() { return "Silla moderna de metal"; }
17    public double precio() { return 45; }
18}
19class MesaModerna implements Mesa {
20    public String describir() { return "Mesa moderna de cristal"; }
21    public double precio() { return 120; }
22}
23class SofaModerno implements Sofa {
24    public String describir() { return "Sofá moderno de piel"; }
25    public double precio() { return 380; }
26}
27
28class Moderno implements FabricaMuebles {
29    public String estilo() { return "moderno"; }
30    public Silla crearSilla() { return new SillaModerna(); }
31    public Mesa crearMesa() { return new MesaModerna(); }
32    public Sofa crearSofa() { return new SofaModerno(); }
33}
34
35class SillaClasica implements Silla {
36    public String describir() { return "Silla clásica de madera tallada"; }
37    public double precio() { return 60; }
38}
39class MesaClasica implements Mesa {
40    public String describir() { return "Mesa clásica de roble"; }
41    public double precio() { return 210; }
42}
43class SofaClasico implements Sofa {
44    public String describir() { return "Sofá clásico de terciopelo"; }
45    public double precio() { return 520; }
46}
47
48class Clasico implements FabricaMuebles {
49    public String estilo() { return "clásico"; }
50    public Silla crearSilla() { return new SillaClasica(); }
51    public Mesa crearMesa() { return new MesaClasica(); }
52    public Sofa crearSofa() { return new SofaClasico(); }
53}
54
55class SillaNordica implements Silla {
56    public String describir() { return "Silla nórdica de pino"; }
57    public double precio() { return 39; }
58}
59class MesaNordica implements Mesa {
60    public String describir() { return "Mesa nórdica de abedul"; }
61    public double precio() { return 99; }
62}
63class SofaNordico implements Sofa {
64    public String describir() { return "Sofá nórdico de lino"; }
65    public double precio() { return 299; }
66}
67
68class Nordico implements FabricaMuebles {
69    public String estilo() { return "nórdico"; }
70    public Silla crearSilla() { return new SillaNordica(); }
71    public Mesa crearMesa() { return new MesaNordica(); }
72    public Sofa crearSofa() { return new SofaNordico(); }
73}
74
75public class Main {
76    static String euros(double x) {
77        return String.format(Locale.ROOT, "%.2f", x).replace('.', ',') + " €";
78    }
79
80    public static void main(String[] args) {
81        Map<String, FabricaMuebles> estilos = new HashMap<>();
82        estilos.put("moderno", new Moderno());
83        estilos.put("clasico", new Clasico());
84        estilos.put("nordico", new Nordico());
85        Scanner sc = new Scanner(System.in);
86        FabricaMuebles fabrica = null;
87        while (sc.hasNextLine()) {
88            String[] p = sc.nextLine().trim().split("\\s+");
89            if (p[0].isEmpty()) continue;
90            if (p[0].equals("estilo") && p.length == 2) {
91                FabricaMuebles f = estilos.get(p[1]);
92                if (f == null) System.out.println("Estilo desconocido: " + p[1]);
93                else {
94                    fabrica = f;
95                    System.out.println("Estilo: " + fabrica.estilo());
96                }
97            } else if (p[0].equals("pedido")) {
98                if (fabrica == null) {
99                    System.out.println("Elige primero un estilo");
100                    continue;
101                }
102                double total = 0;
103                for (int i = 1; i < p.length; i++) {
104                    String descripcion;
105                    double precio;
106                    switch (p[i]) {
107                        case "silla" -> { Silla s = fabrica.crearSilla(); descripcion = s.describir(); precio = s.precio(); }
108                        case "mesa" -> { Mesa m = fabrica.crearMesa(); descripcion = m.describir(); precio = m.precio(); }
109                        case "sofa" -> { Sofa s = fabrica.crearSofa(); descripcion = s.describir(); precio = s.precio(); }
110                        default -> { System.out.println("  Pieza desconocida: " + p[i]); continue; }
111                    }
112                    System.out.println("  " + descripcion + ": " + euros(precio));
113                    total += precio;
114                }
115                System.out.println("Total: " + euros(total));
116            } else {
117                System.out.println("Orden no válida: " + String.join(" ", p));
118            }
119        }
120    }
121}

Cambiar de colección es cambiar la fábrica (fabrica = f); el código que crea las piezas no se entera.

Como todas las piezas de un pedido salen de la misma fábrica, es imposible vender una silla nórdica con una mesa clásica por error.

2. Un documento en tres formatos

El programa escribe un documento pieza a pieza (títulos, párrafos y listas) con la familia del formato elegido. Ya está el formato de texto. Escribe las familias Html y Markdown y regístralas en el main con las claves html y markdown.

  • Órdenes: formato nombre (escribe --- nombre ---, o Formato desconocido: nombre), titulo texto, parrafo texto y lista a | b | c. Se empieza en formato texto.
  • HTML: <h1>texto</h1>, <p>texto</p> y la lista como <ul>, una línea <li>elemento</li> por elemento y </ul>. En el texto, & se escribe &amp;, < es &lt; y > es &gt; (en este orden: primero el &).
  • Markdown: # texto, el párrafo tal cual y una línea - elemento por elemento.
JavaUn documento en tres formatosMedio

Ejemplo

Entrada (lo que se escribe por teclado)
titulo Notas de DAM
parrafo Primera evaluación.
lista Ana: 8 | Luis: 6
formato html
titulo Notas de DAM
parrafo Primera evaluación.
lista Ana: 8 | Luis: 6
formato markdown
titulo Notas de DAM
lista Ana: 8 | Luis: 6
Salida esperada
NOTAS DE DAM
============
Primera evaluación.
  * Ana: 8
  * Luis: 6
--- html ---
<h1>Notas de DAM</h1>
<p>Primera evaluación.</p>
<ul>
  <li>Ana: 8</li>
  <li>Luis: 6</li>
</ul>
--- markdown ---
# Notas de DAM
- Ana: 8
- Luis: 6
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 Titulo { String pintar(String texto); }
4interface Parrafo { String pintar(String texto); }
5interface Lista { String pintar(List<String> elementos); }
6
7/** Una familia por formato de salida: un documento nunca mezcla piezas de dos formatos. */
8interface FabricaDocumento {
9    Titulo crearTitulo();
10    Parrafo crearParrafo();
11    Lista crearLista();
12}
13
14class TituloTexto implements Titulo {
15    public String pintar(String t) { return t.toUpperCase() + "\n" + "=".repeat(t.length()); }
16}
17class ParrafoTexto implements Parrafo {
18    public String pintar(String t) { return t; }
19}
20class ListaTexto implements Lista {
21    public String pintar(List<String> elementos) {
22        StringBuilder sb = new StringBuilder();
23        for (int i = 0; i < elementos.size(); i++) sb.append(i > 0 ? "\n" : "").append("  * ").append(elementos.get(i));
24        return sb.toString();
25    }
26}
27
28class Texto implements FabricaDocumento {
29    public Titulo crearTitulo() { return new TituloTexto(); }
30    public Parrafo crearParrafo() { return new ParrafoTexto(); }
31    public Lista crearLista() { return new ListaTexto(); }
32}
33
34class Html implements FabricaDocumento {
35    static String escapar(String t) {
36        return t.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
37    }
38    public Titulo crearTitulo() { return t -> "<h1>" + escapar(t) + "</h1>"; }
39    public Parrafo crearParrafo() { return t -> "<p>" + escapar(t) + "</p>"; }
40    public Lista crearLista() {
41        return elementos -> {
42            StringBuilder sb = new StringBuilder("<ul>");
43            for (String e : elementos) sb.append("\n  <li>").append(escapar(e)).append("</li>");
44            return sb.append("\n</ul>").toString();
45        };
46    }
47}
48
49class Markdown implements FabricaDocumento {
50    public Titulo crearTitulo() { return t -> "# " + t; }
51    public Parrafo crearParrafo() { return t -> t; }
52    public Lista crearLista() {
53        return elementos -> {
54            List<String> lineas = new ArrayList<>();
55            for (String e : elementos) lineas.add("- " + e);
56            return String.join("\n", lineas);
57        };
58    }
59}
60
61public class Main {
62    public static void main(String[] args) {
63        Map<String, FabricaDocumento> formatos = new HashMap<>();
64        formatos.put("texto", new Texto());
65        formatos.put("html", new Html());
66        formatos.put("markdown", new Markdown());
67        Scanner sc = new Scanner(System.in);
68        FabricaDocumento fabrica = formatos.get("texto");
69        while (sc.hasNextLine()) {
70            String linea = sc.nextLine().trim();
71            if (linea.isEmpty()) continue;
72            int esp = linea.indexOf(' ');
73            String orden = esp < 0 ? linea : linea.substring(0, esp);
74            String resto = esp < 0 ? "" : linea.substring(esp + 1).trim();
75            switch (orden) {
76                case "formato" -> {
77                    FabricaDocumento f = formatos.get(resto);
78                    if (f == null) System.out.println("Formato desconocido: " + resto);
79                    else {
80                        fabrica = f;
81                        System.out.println("--- " + resto + " ---");
82                    }
83                }
84                case "titulo" -> System.out.println(fabrica.crearTitulo().pintar(resto));
85                case "parrafo" -> System.out.println(fabrica.crearParrafo().pintar(resto));
86                case "lista" -> {
87                    List<String> elementos = new ArrayList<>();
88                    for (String e : resto.split("\\|")) if (!e.isBlank()) elementos.add(e.trim());
89                    if (elementos.isEmpty()) System.out.println("Lista vacía");
90                    else System.out.println(fabrica.crearLista().pintar(elementos));
91                }
92                default -> System.out.println("Orden no válida: " + linea);
93            }
94        }
95    }
96}

El main pide un título, un párrafo o una lista a la fábrica actual, y las tres piezas salen siempre del mismo formato.

Cuando los productos tienen un solo método, las fábricas concretas pueden devolver lambdas: el patrón es el mismo con mucho menos código.

Test

Test: Abstract Factory

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é crea una fábrica abstracta?

  2. 2.¿Qué cuesta más en una Abstract Factory?

  3. 3.¿Dónde se decide qué familia se usa?

  4. 4.¿Qué API de Java funciona como una fábrica abstracta?

  5. 5.¿En qué se diferencia de Factory Method?

Relacionado