Apuntes DAM

Patrón Factory Method

Un método que crea los objetos en lugar de hacer new en el código que los usa: cada subclase (o cada caso) decide qué clase concreta crear, y el resto del programa solo conoce la interfaz.

nivel básicoTambién: método fábrica, factoría, fábrica, factory

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.

Factory Method

Elige por qué canales se avisa, en orden, y el texto del aviso. Cada canal decide qué aviso crea.

email, sms o push

Paso 1

Canal tiene la lógica común (notificar) y declara el método fábrica crearAviso() sin decir qué aviso crea: eso lo decide cada subclase.

1abstract class Canal {
2    protected abstract Aviso crearAviso();  // canal = —, avisos creados = 0
3
4    void notificar(String texto) {
5        Aviso aviso = crearAviso();
6        System.out.println(aviso.enviar(texto));
7    }
8}
9
10class CanalSms extends Canal {
11    protected Aviso crearAviso() {
12        return new Sms("600 111 222");
13    }
14}

Variables

canal
—
avisos creados
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

Cada new Email(...) escrito en medio de la lógica ata ese código a la clase Email. Si mañana hay que enviar también SMS o notificaciones al móvil, toca buscar todos los new y llenarlos de if.

El patrón Factory Method saca la creación a un método, el método fábrica, que devuelve el producto por su interfaz (Aviso). En la versión de los libros, una clase abstracta (el creador, Canal) tiene la lógica común y declara el método fábrica abstracto; cada subclase lo implementa creando su producto concreto. La lógica común usa el producto sin saber qué clase es.

En el día a día se usa todavía más la «fábrica simple»: un método estático que recibe un nombre o una configuración y devuelve el objeto adecuado (List.of, Calendar.getInstance, Integer.valueOf). No es exactamente el patrón de los libros, pero resuelve el mismo problema: que solo un sitio del programa sepa qué clase se crea.

Cuándo usarlo

  • El código que usa un objeto no debería saber de qué clase concreta es (avisos, conexiones, exportadores, figuras).
  • Una clase base tiene un algoritmo común y solo cambia el objeto que se crea en un paso.
  • Se quiere poder añadir un tipo nuevo creando una clase, sin tocar el código que ya funciona.
  • Crear el objeto tiene lógica (validar, elegir según la configuración, reutilizar objetos ya creados) que no debería repetirse.

Cuándo no

  • Si solo hay una clase posible y no va a haber más: un new es más claro.
  • Si la jerarquía de creadores solo existe para elegir el producto: una fábrica simple (un método estático o un Map de nombre a constructor) es más ligera.

Participantes

  1. Producto. La interfaz de lo que se crea (Aviso).
  2. Productos concretos. Las clases que la implementan (Email, Sms, Push).
  3. Creador. La clase abstracta (Canal) con la lógica común (notificar) y el método fábrica abstracto (crearAviso), que devuelve un Aviso.
  4. Creadores concretos. Cada subclase (CanalSms…) implementa el método fábrica creando su producto concreto.

Diagrama de clases

usacreacreacrea«interface»Aviso+enviar(texto : String) : StringEmailSmsPush«abstract»Canal+notificar(texto : String) : void#crearAviso() : AvisoCanalEmail#crearAviso() : AvisoCanalSms#crearAviso() : AvisoCanalPush#crearAviso() : Aviso
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
1@startuml
2interface Aviso {
3  +enviar(texto : String) : String
4}
5class Email
6class Sms
7class Push
8abstract class Canal {
9  +notificar(texto : String) : void
10  #{abstract} crearAviso() : Aviso
11}
12class CanalEmail {
13  #crearAviso() : Aviso
14}
15class CanalSms {
16  #crearAviso() : Aviso
17}
18class CanalPush {
19  #crearAviso() : Aviso
20}
21Aviso <|.. Email
22Aviso <|.. Sms
23Aviso <|.. Push
24Canal <|-- CanalEmail
25Canal <|-- CanalSms
26Canal <|-- CanalPush
27Canal ..> Aviso : usa
28CanalEmail ..> Email : crea
29CanalSms ..> Sms : crea
30CanalPush ..> Push : crea
31@enduml

Puedes copiarlo en el editor de diagramas UML y modificarlo.

El código

Un canal por tipo de aviso

notificar está escrito una sola vez en Canal y funciona con cualquier aviso: cada subclase solo dice cuál crea.

Java
1import java.util.*;
2
3/** El producto: lo que crea el método fábrica. */
4interface Aviso {
5    String enviar(String texto);
6}
7
8class Email implements Aviso {
9    private final String direccion;
10    Email(String direccion) { this.direccion = direccion; }
11    public String enviar(String texto) { return "Email a " + direccion + ": " + texto; }
12}
13
14class Sms implements Aviso {
15    private final String telefono;
16    Sms(String telefono) { this.telefono = telefono; }
17    public String enviar(String texto) {
18        String corto = texto.length() > 20 ? texto.substring(0, 17) + "..." : texto;   // un SMS es corto
19        return "SMS a " + telefono + ": " + corto;
20    }
21}
22
23class Push implements Aviso {
24    public String enviar(String texto) { return "Notificación en el móvil: " + texto; }
25}
26
27/** El creador: tiene la lógica común y deja que cada subclase decida qué aviso crear. */
28abstract class Canal {
29    protected abstract Aviso crearAviso();          // el método fábrica
30
31    void notificar(String texto) {
32        Aviso aviso = crearAviso();                  // no sabe (ni le importa) qué clase concreta es
33        System.out.println(aviso.enviar(texto));
34    }
35}
36
37class CanalEmail extends Canal {
38    protected Aviso crearAviso() { return new Email("ana@correo.es"); }
39}
40
41class CanalSms extends Canal {
42    protected Aviso crearAviso() { return new Sms("600 111 222"); }
43}
44
45class CanalPush extends Canal {
46    protected Aviso crearAviso() { return new Push(); }
47}
48
49public class Main {
50    public static void main(String[] args) {
51        List<Canal> canales = List.of(new CanalEmail(), new CanalSms(), new CanalPush());
52        for (Canal canal : canales) canal.notificar("Tu pedido ha salido del almacén");
53    }
54}

Salida al ejecutarlo (la misma en los 5 lenguajes)

Email a ana@correo.es: Tu pedido ha salido del almacén
SMS a 600 111 222: Tu pedido ha sali...
Notificación en el móvil: Tu pedido ha salido del almacén

La fábrica simple: un método estático

La variante que más se ve en el código real y en la biblioteca de Java.

Java
1/** Muchas veces basta un método estático que decide la clase: la «fábrica simple». */
2interface Aviso {
3    String enviar(String texto);
4
5    static Aviso de(String canal) {
6        return switch (canal) {
7            case "email" -> new Email("ana@correo.es");
8            case "sms" -> new Sms("600 111 222");
9            case "push" -> new Push();
10            default -> throw new IllegalArgumentException("Canal desconocido: " + canal);
11        };
12    }
13}
14
15// Quien lo usa no escribe ningún new de una clase concreta:
16Aviso aviso = Aviso.de(preferencias.canal());
17
18// La biblioteca de Java está llena de métodos fábrica estáticos:
19List<String> ciclos = List.of("DAM", "DAW");          // devuelve una lista inmutable de una clase interna
20Integer n = Integer.valueOf(42);                       // puede devolver un objeto ya creado (caché)
21Calendar hoy = Calendar.getInstance();                 // GregorianCalendar u otro, según el país
22ExecutorService pool = Executors.newFixedThreadPool(4);

En la práctica

  • List.of, Map.of, Integer.valueOf, Calendar.getInstance, Path.of, Executors.newFixedThreadPool: métodos fábrica estáticos de la biblioteca.
  • DocumentBuilderFactory.newInstance().newDocumentBuilder() para leer XML y DriverManager.getConnection en JDBC: devuelven la implementación que toca sin que escribas su clase.
  • En Android, Fragment.newInstance(...) y Intent.createChooser(...); en Laravel y Spring, las factorías de modelos para las pruebas.
  • Los juegos crean enemigos, disparos o niveles con métodos fábrica para poder añadir tipos sin tocar el bucle principal.

Errores típicos

  • Mover el switch de los new a la fábrica y después copiarlo en otros sitios: la fábrica tiene que ser el único que lo sabe.
  • Que el método fábrica devuelva la clase concreta (Email crearAviso()) en vez de la interfaz: el código que lo usa vuelve a depender de ella.
  • Crear una jerarquía de creadores con una subclase por producto cuando una fábrica simple con un Map bastaba.
  • Devolver null cuando el tipo no existe: mejor lanzar una excepción con un mensaje claro (Canal desconocido: fax).

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. Fábrica de figuras

El programa lee figuras y escribe su área y su perímetro. Todo pasa por la fábrica Figuras.crear(linea), que de momento solo sabe crear círculos. Complétala para que cree rectángulos, cuadrados (que son un Rectangulo con los dos lados iguales: no hace falta otra clase) y triángulos, y para que rechace las líneas que no valen. El main y las figuras ya están escritos.

  • Cada línea: circulo r, rectangulo b h, cuadrado l o triangulo a b c. Salida: nombre: área A, perímetro P con dos decimales y coma. Al final, Total: N figuras, área T.
  • Errores (los mensajes los da la fábrica con una IllegalArgumentException): Figura desconocida: tipo, Faltan o sobran medidas: línea, Medida no válida: línea (no es un número o no es mayor que 0) y Triángulo imposible: línea (un lado es mayor o igual que la suma de los otros dos).
JavaFábrica de figurasFácil

Ejemplo

Entrada (lo que se escribe por teclado)
circulo 2
rectangulo 3 4
cuadrado 2.5
triangulo 3 4 5
Salida esperada
círculo de radio 2: área 12,57, perímetro 12,57
rectángulo 3 × 4: área 12,00, perímetro 14,00
rectángulo 2,5 × 2,5: área 6,25, perímetro 10,00
triángulo 3, 4, 5: área 6,00, perímetro 12,00
Total: 4 figuras, área 36,82
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.math.BigDecimal;
2import java.util.*;
3
4interface Figura {
5    String nombre();
6    double area();
7    double perimetro();
8}
9
10record Circulo(double r) implements Figura {
11    public String nombre() { return "círculo de radio " + Main.num(r); }
12    public double area() { return Math.PI * r * r; }
13    public double perimetro() { return 2 * Math.PI * r; }
14}
15
16record Rectangulo(double base, double altura) implements Figura {
17    public String nombre() { return "rectángulo " + Main.num(base) + " × " + Main.num(altura); }
18    public double area() { return base * altura; }
19    public double perimetro() { return 2 * (base + altura); }
20}
21
22record Triangulo(double a, double b, double c) implements Figura {
23    public String nombre() { return "triángulo " + Main.num(a) + ", " + Main.num(b) + ", " + Main.num(c); }
24    public double area() {
25        double s = perimetro() / 2;                    // fórmula de Herón
26        return Math.sqrt(s * (s - a) * (s - b) * (s - c));
27    }
28    public double perimetro() { return a + b + c; }
29}
30
31/** La fábrica: el único sitio del programa que sabe qué clase corresponde a cada nombre. */
32class Figuras {
33    static Figura crear(String linea) {
34        String[] p = linea.trim().split("\\s+");
35        int n = switch (p[0]) {
36            case "circulo", "cuadrado" -> 1;
37            case "rectangulo" -> 2;
38            case "triangulo" -> 3;
39            default -> throw new IllegalArgumentException("Figura desconocida: " + p[0]);
40        };
41        if (p.length - 1 != n) throw new IllegalArgumentException("Faltan o sobran medidas: " + linea.trim());
42        double[] m = new double[n];
43        for (int i = 0; i < n; i++) {
44            try {
45                m[i] = Double.parseDouble(p[i + 1]);
46            } catch (NumberFormatException e) {
47                throw new IllegalArgumentException("Medida no válida: " + linea.trim());
48            }
49            if (!(m[i] > 0) || Double.isInfinite(m[i])) throw new IllegalArgumentException("Medida no válida: " + linea.trim());
50        }
51        return switch (p[0]) {
52            case "circulo" -> new Circulo(m[0]);
53            case "cuadrado" -> new Rectangulo(m[0], m[0]);           // no hace falta una clase nueva
54            case "rectangulo" -> new Rectangulo(m[0], m[1]);
55            default -> {
56                double a = m[0], b = m[1], c = m[2];
57                if (a + b <= c || a + c <= b || b + c <= a) throw new IllegalArgumentException("Triángulo imposible: " + linea.trim());
58                yield new Triangulo(a, b, c);
59            }
60        };
61    }
62}
63
64public class Main {
65    static String num(double x) {
66        return new BigDecimal(Double.toString(x)).stripTrailingZeros().toPlainString().replace('.', ',');
67    }
68
69    static String dos(double x) {
70        return String.format(Locale.ROOT, "%.2f", x).replace('.', ',');
71    }
72
73    public static void main(String[] args) {
74        Scanner sc = new Scanner(System.in);
75        List<Figura> figuras = new ArrayList<>();
76        while (sc.hasNextLine()) {
77            String linea = sc.nextLine();
78            if (linea.isBlank()) continue;
79            try {
80                Figura f = Figuras.crear(linea);
81                figuras.add(f);
82                System.out.println(f.nombre() + ": área " + dos(f.area()) + ", perímetro " + dos(f.perimetro()));
83            } catch (IllegalArgumentException e) {
84                System.out.println(e.getMessage());
85            }
86        }
87        double total = 0;
88        for (Figura f : figuras) total += f.area();
89        System.out.println("Total: " + figuras.size() + (figuras.size() == 1 ? " figura" : " figuras") + ", área " + dos(total));
90    }
91}

El main no tiene ni un new de una figura: si mañana hay hexágonos, solo cambia la fábrica.

La fábrica también es el sitio natural para validar: ningún objeto Triangulo imposible llega a existir.

2. Exportar una tabla en varios formatos

Exportador.exportar escribe una tabla siempre igual: cabecera, filas y pie. Lo que cambia es el Formato, que cada exportador crea con su método fábrica crearFormato. Ya está el CSV: escribe los formatos Markdown y Json con sus exportadores y regístralos en el main con las claves markdown y json.

  • Órdenes: columnas a;b;c (empieza una tabla nueva), fila x;y;z (tantos valores como columnas, o Fila no válida: x;y), exportar formato (escribe --- formato --- y la tabla, Formato desconocido: pdf o Faltan las columnas).
  • Markdown: | a | b |, después | --- | --- |, una línea así por fila, con cada | de un valor escrito como \|, y al final _N filas_ (_1 fila_).
  • JSON: [, cada fila en su línea como {"columna": "valor", ...} separadas por coma, y ]. En los textos, \ se escribe \\ y " se escribe \". Una tabla sin filas es [ y ].
JavaExportar una tabla en varios formatosMedio

Ejemplo

Entrada (lo que se escribe por teclado)
columnas nombre;ciclo;nota
fila Ana;DAM;8.5
fila Luis;DAW;6
exportar csv
exportar markdown
exportar json
Salida esperada
--- csv ---
nombre;ciclo;nota
Ana;DAM;8.5
Luis;DAW;6
--- markdown ---
| nombre | ciclo | nota |
| --- | --- | --- |
| Ana | DAM | 8.5 |
| Luis | DAW | 6 |
_2 filas_
--- json ---
[
  {"nombre": "Ana", "ciclo": "DAM", "nota": "8.5"},
  {"nombre": "Luis", "ciclo": "DAW", "nota": "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
3/** El producto: escribe una tabla en un formato. Se crea uno nuevo para cada exportación. */
4interface Formato {
5    String cabecera(List<String> columnas);
6    String fila(List<String> valores);
7    String pie(int filas);
8}
9
10/** El creador: el algoritmo de exportar es siempre el mismo; el formato lo decide cada subclase. */
11abstract class Exportador {
12    protected abstract Formato crearFormato();          // el método fábrica
13
14    final String exportar(List<String> columnas, List<List<String>> filas) {
15        Formato f = crearFormato();
16        StringBuilder sb = new StringBuilder(f.cabecera(columnas));
17        for (List<String> fila : filas) sb.append(f.fila(fila));
18        sb.append(f.pie(filas.size()));
19        return sb.toString();
20    }
21}
22
23class Csv implements Formato {
24    private static String celda(String v) {
25        return v.contains("\"") ? "\"" + v.replace("\"", "\"\"") + "\"" : v;
26    }
27    public String cabecera(List<String> columnas) { return fila(columnas); }
28    public String fila(List<String> valores) {
29        List<String> celdas = new ArrayList<>();
30        for (String v : valores) celdas.add(celda(v));
31        return String.join(";", celdas) + "\n";
32    }
33    public String pie(int filas) { return ""; }
34}
35
36class ExportadorCsv extends Exportador {
37    protected Formato crearFormato() { return new Csv(); }
38}
39
40class Markdown implements Formato {
41    private static String celda(String v) { return v.replace("|", "\\|"); }
42
43    private static String linea(List<String> valores) {
44        StringBuilder sb = new StringBuilder("|");
45        for (String v : valores) sb.append(" ").append(celda(v)).append(" |");
46        return sb.append("\n").toString();
47    }
48
49    public String cabecera(List<String> columnas) {
50        return linea(columnas) + "|" + " --- |".repeat(columnas.size()) + "\n";
51    }
52    public String fila(List<String> valores) { return linea(valores); }
53    public String pie(int filas) { return "_" + filas + (filas == 1 ? " fila_" : " filas_") + "\n"; }
54}
55
56/** Guarda las columnas y si ya ha escrito alguna fila: por eso cada exportación usa un formato nuevo. */
57class Json implements Formato {
58    private List<String> columnas;
59    private boolean primera = true;
60
61    private static String texto(String v) {
62        return "\"" + v.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
63    }
64
65    public String cabecera(List<String> columnas) {
66        this.columnas = columnas;
67        return "[\n";
68    }
69    public String fila(List<String> valores) {
70        List<String> pares = new ArrayList<>();
71        for (int i = 0; i < columnas.size(); i++) pares.add(texto(columnas.get(i)) + ": " + texto(valores.get(i)));
72        String s = (primera ? "" : ",\n") + "  {" + String.join(", ", pares) + "}";
73        primera = false;
74        return s;
75    }
76    public String pie(int filas) { return (filas > 0 ? "\n" : "") + "]\n"; }
77}
78
79class ExportadorMarkdown extends Exportador {
80    protected Formato crearFormato() { return new Markdown(); }
81}
82
83class ExportadorJson extends Exportador {
84    protected Formato crearFormato() { return new Json(); }
85}
86
87public class Main {
88    public static void main(String[] args) {
89        Map<String, Exportador> exportadores = new LinkedHashMap<>();
90        exportadores.put("csv", new ExportadorCsv());
91        exportadores.put("markdown", new ExportadorMarkdown());
92        exportadores.put("json", new ExportadorJson());
93
94        Scanner sc = new Scanner(System.in);
95        List<String> columnas = null;
96        List<List<String>> filas = new ArrayList<>();
97        while (sc.hasNextLine()) {
98            String linea = sc.nextLine().trim();
99            if (linea.isEmpty()) continue;
100            int esp = linea.indexOf(' ');
101            String orden = esp < 0 ? linea : linea.substring(0, esp);
102            String resto = esp < 0 ? "" : linea.substring(esp + 1).trim();
103            switch (orden) {
104                case "columnas" -> {
105                    columnas = List.of(resto.split(";", -1));
106                    filas.clear();
107                }
108                case "fila" -> {
109                    List<String> valores = List.of(resto.split(";", -1));
110                    if (columnas == null || valores.size() != columnas.size()) System.out.println("Fila no válida: " + resto);
111                    else filas.add(valores);
112                }
113                case "exportar" -> {
114                    Exportador e = exportadores.get(resto);
115                    if (e == null) System.out.println("Formato desconocido: " + resto);
116                    else if (columnas == null) System.out.println("Faltan las columnas");
117                    else {
118                        System.out.println("--- " + resto + " ---");
119                        System.out.print(e.exportar(columnas, filas));
120                    }
121                }
122                default -> System.out.println("Orden no válida: " + linea);
123            }
124        }
125    }
126}

exportar está escrito una vez y es final: los formatos solo rellenan sus tres piezas. Esa forma (un algoritmo fijo que llama a pasos que cambian) es también el patrón Template Method.

Que el método fábrica cree un objeto nuevo en cada llamada permite que el formato guarde estado (la coma del JSON) sin que una exportación estropee la siguiente.

Test

Test: Factory Method

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é devuelve el método fábrica?

  2. 2.En la versión de los libros, ¿quién decide qué producto concreto se crea?

  3. 3.¿Cuál de estos es un método fábrica estático de la biblioteca de Java?

  4. 4.Quieres añadir avisos por Telegram al ejemplo de los canales. ¿Qué cambias?

  5. 5.¿Qué ventaja tiene validar dentro de la fábrica?

Relacionado