Si el módulo tuviera que reducirse a un solo patrón, sería este: Factory Method es la respuesta más directa a la pregunta que abrió el módulo —¿quién decide qué clase concreta se instancia?— y la puerta de entrada a toda la mentalidad creacional. En esta lección lo construiremos sobre el sistema de notificaciones de PideYa (push, SMS, email), lo distinguiremos de su primo popular pero no catalogado (la simple factory), y terminaremos con las variantes modernas que los genéricos y las lambdas de Java han traído al patrón.

Contenido

  1. El problema en PideYa
  2. Primer paso: la simple factory (que no es un patrón GoF)
  3. El patrón Factory Method: estructura GoF
  4. Implementación Java completa
  5. Variantes modernas: genéricos y lambdas
  6. Cuándo usarlo y cuándo no
  7. Relación con otros patrones
  8. Errores comunes, ejercicios y conclusión

El problema en PideYa

Recupera el síntoma de la introducción del módulo: PideYa notifica a sus clientes por el canal que cada uno prefiere, y la decisión de qué notificador crear está repetida, con este aspecto, en el servicio de pedidos, el de reparto y el de promociones:

// Repetido (con pequeñas variaciones que ya divergen) en TRES servicios:
Notificador n;
switch (cliente.getCanalPreferido()) {
    case PUSH:  n = new NotificadorPush(); break;
    case SMS:   n = new NotificadorSms(claveTwilio); break;
    case EMAIL: n = new NotificadorEmail(servidorSmtp); break;
    default: throw new IllegalArgumentException();
}
n.enviar(cliente, mensaje);

Los dolores concretos:

  • Duplicación (violación de DRY): añadir el canal WhatsApp, que negocio acaba de pedir, exige localizar y editar los tres switch. En uno se olvidará.
  • Violación de OCP: cada canal nuevo modifica código estable en varios sitios.
  • Conocimiento de construcción disperso: la clave de Twilio y el servidor SMTP aparecen allá donde se copie el bloque.

La buena noticia: el código cliente ya programa contra la interfaz Notificador para usar el objeto. Solo falla en el momento de crearlo. Necesitamos extraer ese momento a un lugar único y extensible.

Primer paso: la simple factory (que no es un patrón GoF)

El primer reflejo de cualquier desarrollador es mover el switch a una clase con un método estático:

public class FabricaNotificadores {

    public static Notificador crear(CanalNotificacion canal) {
        return switch (canal) {
            case PUSH  -> new NotificadorPush();
            case SMS   -> new NotificadorSms(ConfigMensajeria.claveTwilio());
            case EMAIL -> new NotificadorEmail(ConfigMensajeria.servidorSmtp());
        };
    }
}

// En los tres servicios, el bloque entero se reduce a:
Notificador n = FabricaNotificadores.crear(cliente.getCanalPreferido());

Esto se llama simple factory (o static factory) y conviene decirlo alto y claro: no es un patrón del catálogo GoF; es un idiom, un honesto paso de refactorización. Y no lo desprecies: ya ha resuelto dos de los tres dolores (la duplicación desaparece y el conocimiento de construcción se centraliza). Para muchísimos casos reales, aquí puedes parar, y hacerlo es un acto de sano antisobreingeniería.

Lo que la simple factory no resuelve: el switch sigue existiendo, en un único sitio pero cerrado. Añadir WhatsApp sigue siendo modificar la fábrica (OCP herido, aunque ahora el daño es local), y sobre todo: nadie puede extender el sistema de canales sin tocar el código central —un problema serio si, por ejemplo, los módulos de PideYa los mantienen equipos distintos, o si el núcleo de notificaciones se distribuye como librería a las apps de restaurantes—. Cuando ese eje de extensión es real, entra el patrón de verdad.

El patrón Factory Method: estructura GoF

Intención (GoF): definir una interfaz para crear un objeto, dejando que las subclases decidan qué clase instanciar. Factory Method permite a una clase delegar la instanciación en sus subclases.

La idea-fuerza: el código general (el creador) contiene toda la lógica de negocio que usa el producto, pero deja un hueco —un método de fabricación, el factory method— que cada subclase rellena decidiendo el producto concreto. Es OCP en estado puro: para añadir un canal, se añade una pareja de clases; no se modifica nada existente. Y es el único creacional de ámbito de clase: la variación se consigue heredando.

classDiagram
    class ServicioNotificaciones {
        <<abstract>>
        +notificarCambioPedido(Pedido)
        #crearNotificador()* Notificador
    }
    class ServicioNotificacionesPush {
        #crearNotificador() Notificador
    }
    class ServicioNotificacionesSms {
        #crearNotificador() Notificador
    }
    class Notificador {
        <<interface>>
        +enviar(Cliente, String)
    }
    class NotificadorPush {
        +enviar(Cliente, String)
    }
    class NotificadorSms {
        +enviar(Cliente, String)
    }
    ServicioNotificaciones <|-- ServicioNotificacionesPush
    ServicioNotificaciones <|-- ServicioNotificacionesSms
    Notificador <|.. NotificadorPush
    Notificador <|.. NotificadorSms
    ServicioNotificaciones ..> Notificador : usa lo que crea
    ServicioNotificacionesPush ..> NotificadorPush : crea
    ServicioNotificacionesSms ..> NotificadorSms : crea

Los cuatro roles GoF, con su encarnación en PideYa:

Rol GoF Papel En PideYa
Product Interfaz de lo que se fabrica Notificador
ConcreteProduct Implementaciones concretas NotificadorPush, NotificadorSms, NotificadorEmail
Creator Clase (a menudo abstracta) con la lógica común; declara el factory method ServicioNotificaciones
ConcreteCreator Subclase que implementa el factory method y elige el producto ServicioNotificacionesPush, ServicioNotificacionesSms, ...

Implementación Java completa

El producto y sus implementaciones (sin cambios respecto a lo que PideYa ya tenía):

public interface Notificador {
    void enviar(Cliente cliente, String mensaje);
}

public class NotificadorPush implements Notificador {
    @Override
    public void enviar(Cliente cliente, String mensaje) {
        // Envío mediante el servicio de push a la app del cliente
        System.out.println("[PUSH a " + cliente.getIdDispositivo() + "] " + mensaje);
    }
}

public class NotificadorSms implements Notificador {
    private final String claveTwilio;

    public NotificadorSms(String claveTwilio) { this.claveTwilio = claveTwilio; }

    @Override
    public void enviar(Cliente cliente, String mensaje) {
        System.out.println("[SMS a " + cliente.getTelefono() + "] " + mensaje);
    }
}

public class NotificadorEmail implements Notificador {
    private final String servidorSmtp;

    public NotificadorEmail(String servidorSmtp) { this.servidorSmtp = servidorSmtp; }

    @Override
    public void enviar(Cliente cliente, String mensaje) {
        System.out.println("[EMAIL a " + cliente.getEmail() + "] " + mensaje);
    }
}

El creador: fíjate en que contiene lógica de negocio real (componer el mensaje, decidir cuándo notificar, registrar el envío) que es común a todos los canales. El patrón brilla justo por esto: no es "una clase que solo fabrica", es una clase con trabajo propio que delega solo la decisión de instanciación:

public abstract class ServicioNotificaciones {

    /** Lógica de negocio común: igual para todos los canales. */
    public void notificarCambioPedido(Pedido pedido) {
        String mensaje = componerMensaje(pedido);          // trabajo común
        Notificador notificador = crearNotificador();      // <-- EL factory method
        notificador.enviar(pedido.getCliente(), mensaje);  // uso del producto
        RegistroEventos.INSTANCIA.registrar(               // trabajo común
                "Notificado pedido " + pedido.getId());
    }

    private String componerMensaje(Pedido pedido) {
        return "Tu pedido de " + pedido.getRestaurante().getNombre()
                + " está " + pedido.getEstado().enTextoAmigable();
    }

    /** El hueco que cada subclase rellena: qué notificador concreto usar. */
    protected abstract Notificador crearNotificador();
}

Los creadores concretos, triviales a propósito —toda su razón de ser es una línea—:

public class ServicioNotificacionesPush extends ServicioNotificaciones {
    @Override
    protected Notificador crearNotificador() {
        return new NotificadorPush();
    }
}

public class ServicioNotificacionesSms extends ServicioNotificaciones {
    private final String claveTwilio;

    public ServicioNotificacionesSms(String claveTwilio) { this.claveTwilio = claveTwilio; }

    @Override
    protected Notificador crearNotificador() {
        return new NotificadorSms(claveTwilio);   // el detalle de construcción vive aquí
    }
}

El cliente elige (o recibe inyectado, mejor) el servicio adecuado y trabaja siempre contra la clase base:

ServicioNotificaciones servicio = new ServicioNotificacionesSms(config.getClaveTwilio());
servicio.notificarCambioPedido(pedido);   // sin saber ni importarle el canal

¿Qué hemos comprado? Añadir WhatsApp ahora es crear NotificadorWhatsApp + ServicioNotificacionesWhatsApp: dos clases nuevas, cero modificaciones. El switch ha desaparecido del mapa (queda, como mucho, un único punto en la raíz de composición que elige qué ServicioNotificaciones construir por cliente, y hasta ese punto puede eliminarse con el registro que veremos en las variantes). Y en los tests, un ServicioNotificacionesDePrueba cuyo factory method devuelve un notificador espía permite verificar la lógica común sin enviar nada real.

Una variante habitual del creador: dar al factory method una implementación por defecto (por ejemplo, return new NotificadorPush()) en vez de dejarlo abstracto. Así el creador es usable tal cual y las subclases solo existen para cambiar el producto. También es común el factory method parametrizado, que recibe un argumento y elige entre varios productos: a medio camino hacia la simple factory.

Variantes modernas: genéricos y lambdas

Con genéricos: el creador conoce el tipo exacto

Cuando el creador debe devolver el tipo concreto sin obligar al cliente a hacer casts:

public abstract class ServicioNotificaciones<T extends Notificador> {
    protected abstract T crearNotificador();
}

public class ServicioNotificacionesPush extends ServicioNotificaciones<NotificadorPush> {
    @Override
    protected NotificadorPush crearNotificador() { return new NotificadorPush(); }
}

Java permite además la covarianza del tipo de retorno sin genéricos: una subclase puede estrechar el retorno de Notificador a NotificadorPush directamente. Los genéricos aportan cuando el tipo del producto viaja por más métodos de la clase.

Con lambdas: el factory method como objeto

Desde Java 8, "un método que crea un Notificador" tiene nombre propio: Supplier<Notificador> (o Function<X, Notificador> si la creación necesita datos). Esto permite una versión composicional del patrón: en lugar de una subclase por producto, el creador recibe la función de fabricación:

public class ServicioNotificaciones {   // ¡ya no es abstracta ni tiene subclases!

    private final Supplier<Notificador> fabricaNotificador;

    public ServicioNotificaciones(Supplier<Notificador> fabricaNotificador) {
        this.fabricaNotificador = fabricaNotificador;
    }

    public void notificarCambioPedido(Pedido pedido) {
        Notificador n = fabricaNotificador.get();     // el "hueco", ahora inyectado
        n.enviar(pedido.getCliente(), componerMensaje(pedido));
    }
    // ...
}

// Configuración en la raíz de composición: cada "subclase" es ahora una línea
var porPush = new ServicioNotificaciones(NotificadorPush::new);
var porSms  = new ServicioNotificaciones(() -> new NotificadorSms(config.getClaveTwilio()));

Observa el desplazamiento conceptual: el patrón GoF varía por herencia (ámbito de clase); la variante con lambdas varía por composición (se inyecta la fábrica), alineándose con la máxima "composición sobre herencia". En Java moderno esta forma es frecuentísima, y es la misma idea que explota el registro de fábricas, que convierte el switch residual en un mapa abierto a extensión:

public class RegistroNotificadores {

    private final Map<CanalNotificacion, Supplier<Notificador>> fabricas =
            new EnumMap<>(CanalNotificacion.class);

    public void registrar(CanalNotificacion canal, Supplier<Notificador> fabrica) {
        fabricas.put(canal, fabrica);
    }

    public Notificador crear(CanalNotificacion canal) {
        Supplier<Notificador> fabrica = fabricas.get(canal);
        if (fabrica == null) throw new IllegalArgumentException("Canal sin registrar: " + canal);
        return fabrica.get();
    }
}

// Arranque de PideYa: registrar es AÑADIR una línea, no editar un switch
registro.registrar(CanalNotificacion.PUSH,  NotificadorPush::new);
registro.registrar(CanalNotificacion.SMS,   () -> new NotificadorSms(config.getClaveTwilio()));
registro.registrar(CanalNotificacion.EMAIL, () -> new NotificadorEmail(config.getServidorSmtp()));

Cada módulo de PideYa (incluso un plugin de terceros) puede registrar sus canales sin que el núcleo los conozca. El JDK está lleno de esta filosofía: los métodos estáticos de fabricación como List.of(...), Optional.of(...) o Files.newBufferedReader(...) son parientes de la simple factory, y las factorías inyectables tipo Supplier aparecen por todo el API de streams y colecciones.

Cuándo usarlo y cuándo no

Úsalo cuando:

  • Una clase con lógica común no puede (ni debe) anticipar la clase concreta de los objetos que necesita, y ese eje de variación es real (canales de notificación de PideYa: reales y crecientes).
  • Quieres que terceros o módulos independientes extiendan tu sistema con nuevos productos sin tocar el núcleo (frameworks: es el patrón de los hot spots de extensión).
  • Necesitas que la construcción del producto quede junto a la variante que lo usa, no dispersa.

No lo uses cuando:

  • Hay un solo producto y ninguna variación a la vista: un new bien situado o una simple factory bastan. Recuerda la escala de la lección: new directo → simple factory → Factory Method; sube un peldaño solo cuando el actual duela.
  • Lo que varía no es un producto sino familias enteras de productos que deben ser coherentes entre sí: ese es el territorio de la siguiente lección.
  • La complejidad está en el proceso de construcción (muchos pasos y opciones), no en la elección de la clase: eso pide Builder.

Relación con otros patrones

Solo como mención, para tu mapa mental: Abstract Factory se implementa habitualmente como un conjunto de factory methods; Template Method es el patrón hermano —notificarCambioPedido es de hecho un método plantilla cuyo paso variable es la creación—; Prototype es la alternativa que evita la jerarquía de creadores clonando ejemplares; y las fábricas suelen exponerse como Singleton o, mejor, inyectadas.

Errores Comunes y Consejos

  • Llamar "Factory Method" a cualquier clase con "Factory" en el nombre. La simple factory estática no es el patrón GoF; no es un pecado usarla (¡al contrario!), pero en una entrevista o una revisión conviene distinguir: el patrón GoF implica un creador extensible cuya variación decide el producto.
  • Crear jerarquías de creadores sin lógica común. Si ServicioNotificaciones no tuviera ningún trabajo propio, la jerarquía entera sería ceremonia: el patrón compensa cuando el creador aporta lógica que reutilizan todas las variantes. Sin ella, un Supplier inyectado da lo mismo con una clase menos.
  • Dejar el switch vivo dentro del "factory method". Un crearNotificador() con un switch dentro de cada subclase indica que la separación por subclases no se hizo de verdad. Cada ConcreteCreator debe ser aburridamente simple.
  • Ignorar la construcción con dependencias. Los productos reales necesitan claves, configuración, colaboradores. Fíjate dónde los pusimos: en el creador concreto (o en la lambda registrada), nunca en el cliente ni en la clase base.
  • Consejo: nombra los factory methods con intención (crearNotificador, no getNotificador): "crear" comunica que cada llamada puede fabricar una instancia nueva, mientras que "get" sugiere devolver algo ya existente.

Ejercicios

Ejercicio 1: añadir un canal sin tocar nada

Partiendo de la implementación completa de la sección 4, añade el canal WhatsApp (necesita un tokenMeta para construirse). Escribe las clases nuevas y señala qué ficheros existentes has tenido que modificar.

Ejercicio 2: refactorizar a registro con lambdas

El módulo de PideYa "avisos a repartidores" tiene esta fábrica cerrada:

public class FabricaAvisosRepartidor {
    public static AvisoRepartidor crear(TipoAviso tipo) {
        switch (tipo) {
            case NUEVO_PEDIDO: return new AvisoNuevoPedido();
            case CANCELACION:  return new AvisoCancelacion();
            default: throw new IllegalArgumentException();
        }
    }
}

Refactorízala a un registro basado en Supplier<AvisoRepartidor> que permita al futuro módulo "propinas" registrar su AvisoPropinaRecibida sin editar esta clase.

Ejercicio 3: identificar los roles

En el JDK, java.util.Calendar.getInstance() devuelve un GregorianCalendar u otra subclase según la configuración regional, y en las colecciones, Iterable.iterator() obliga a cada colección a decidir qué Iterator concreto devuelve. Para cada caso, identifica qué rol GoF cumple cada pieza y razona cuál de los dos es un Factory Method canónico y cuál se parece más a una simple factory.

Soluciones

Solución 1:

public class NotificadorWhatsApp implements Notificador {
    private final String tokenMeta;
    public NotificadorWhatsApp(String tokenMeta) { this.tokenMeta = tokenMeta; }
    @Override
    public void enviar(Cliente cliente, String mensaje) {
        System.out.println("[WHATSAPP a " + cliente.getTelefono() + "] " + mensaje);
    }
}

public class ServicioNotificacionesWhatsApp extends ServicioNotificaciones {
    private final String tokenMeta;
    public ServicioNotificacionesWhatsApp(String tokenMeta) { this.tokenMeta = tokenMeta; }
    @Override
    protected Notificador crearNotificador() { return new NotificadorWhatsApp(tokenMeta); }
}

Ficheros existentes modificados: ninguno de la lógica del patrón. Solo la raíz de composición (el lugar que decide qué servicio recibe cada cliente) conocerá la nueva opción, que es exactamente el punto diseñado para cambiar. Eso es OCP funcionando.

Solución 2:

public class RegistroAvisosRepartidor {

    private final Map<TipoAviso, Supplier<AvisoRepartidor>> fabricas =
            new EnumMap<>(TipoAviso.class);

    public void registrar(TipoAviso tipo, Supplier<AvisoRepartidor> fabrica) {
        fabricas.put(tipo, fabrica);
    }

    public AvisoRepartidor crear(TipoAviso tipo) {
        Supplier<AvisoRepartidor> f = fabricas.get(tipo);
        if (f == null) throw new IllegalArgumentException("Tipo sin registrar: " + tipo);
        return f.get();
    }
}

// Núcleo, en el arranque:
registro.registrar(TipoAviso.NUEVO_PEDIDO, AvisoNuevoPedido::new);
registro.registrar(TipoAviso.CANCELACION,  AvisoCancelacion::new);

// Módulo "propinas", en SU arranque, sin tocar el núcleo:
registro.registrar(TipoAviso.PROPINA, AvisoPropinaRecibida::new);

(Nota honesta: añadir PROPINA al enum TipoAviso sí toca un fichero compartido; si eso molestara, la clave del mapa pasaría a ser un String o un tipo registrable, con el coste en seguridad de tipos que ello implica.)

Solución 3: en Iterable.iterator(): Creator = Iterable/Collection (con muchísima lógica común en AbstractCollection que usa el iterador), ConcreteCreator = ArrayList, HashSet..., Product = Iterator, ConcreteProduct = el iterador interno de cada colección. Es el Factory Method canónico: la subclase decide el producto y el código común lo consume. Calendar.getInstance(), en cambio, es un método estático que decide internamente qué subclase devolver según la configuración: pese al nombre ilustre, funciona como una simple factory (no hay subclases del creador decidiendo el producto).

Conclusión

Factory Method te ha dado el movimiento fundamental de la familia: extraer la decisión de instanciación a un punto con nombre —un método de fabricación— y hacer ese punto extensible, sea por herencia (la forma GoF, ámbito de clase) o por composición con Supplier y registros (la forma moderna). También has aprendido a situar la simple factory en su justo lugar: un idiom valiosísimo que no es el patrón, y el primer peldaño de una escalera (new → simple factory → Factory Method) que solo se sube cuando el peldaño actual duele.

Pero nuestro factory method fabrica productos de uno en uno, y hay problemas donde eso no basta: cuando PideYa cruce fronteras necesitará crear conjuntos de objetos que deben ser coherentes entre sí —pasarela de pago, impuestos y ticket del mismo país, sin mezclas—. Fabricar familias completas es el trabajo de la Abstract Factory.

Curso de Patrones de Diseño de Software

Módulo 1: Introducción a los Patrones de Diseño

Módulo 2: Patrones Creacionales

Módulo 3: Patrones Estructurales

Módulo 4: Patrones de Comportamiento

Módulo 5: Aplicación de Patrones de Diseño

Módulo 6: Patrones de Diseño Avanzados

Módulo 7: Recursos Adicionales y Conclusión

© Copyright 2026. Todos los derechos reservados