Patrones de Diseño: Factory

Patrones de Diseño: Factory

Tabla de Contenido

La instanciación directa de clases concretas es uno de los puntos más costosos de acoplamiento en un diseño orientado a objetos. Cada new o llamada explícita a un constructor ata al cliente a una implementación específica, dificultando la extensión y la sustitución de componentes. Los patrones factory encapsulan esa decisión en un lugar controlado, permitiendo evolucionar la creación de objetos sin propagar cambios a todo el código consumidor.

Note

🚀 Los patrones factory encapsulan la creación de objetos y desacoplan al cliente de las clases concretas que necesita instanciar.

Este artículo forma parte de la serie de patrones de diseño. Analizaremos tres variantes de fabricación (Simple Factory, Factory Method y Abstract Factory), su relación con el Principio de Inversión de Dependencias y los trade-offs que conviene considerar al elegir uno u otro.

Simple Factory

El Simple Factory no es un patrón de diseño formal del catálogo GoF, sino un idiom recurrente: encapsular la lógica de creación de objetos dentro de un método (usualmente estático) que decide qué instancia devolver en función de un parámetro. Es un buen punto de partida para entender los patrones que veremos a continuación.

Info

GoF significa “Gang of Four” (la “Banda de los Cuatro”): los cuatro autores del libro Design Patterns (1994) que catalogó los 23 patrones de diseño clásicos. Cuando decimos que un patrón “es GoF”, significa que aparece en ese catálogo original.

Supongamos una pizzería que produce distintos tipos de pizzas. Sin un factory, el cliente tendría que conocer todas las clases concretas y decidir cuál instanciar. Con un SimpleFactory esa decisión queda centralizada:

classDiagram
    class PizzaStore {
        -factory: SimplePizzaFactory
        +order_pizza(kind) Pizza
    }
    class SimplePizzaFactory {
        +create_pizza(kind) Pizza
    }
    class Pizza {
        <<abstract>>
        +prepare()
        +bake()
        +box()
    }
    class CheesePizza
    class VeggiePizza
    class PepperoniPizza

    PizzaStore --> SimplePizzaFactory : usa
    SimplePizzaFactory ..> Pizza : crea
    Pizza <|-- CheesePizza
    Pizza <|-- VeggiePizza
    Pizza <|-- PepperoniPizza

Implementación mínima en Python:

from abc import ABC, abstractmethod


class Pizza(ABC):
    @abstractmethod
    def prepare(self) -> None: ...

    def bake(self) -> None:
        print("Horneando la pizza")

    def box(self) -> None:
        print("Empacando la pizza")


class CheesePizza(Pizza):
    def prepare(self) -> None:
        print("Preparando pizza de queso")


class VeggiePizza(Pizza):
    def prepare(self) -> None:
        print("Preparando pizza vegetariana")


class SimplePizzaFactory:
    def create_pizza(self, kind: str) -> Pizza:
        if kind == "cheese":
            return CheesePizza()
        if kind == "veggie":
            return VeggiePizza()
        raise ValueError(f"Tipo de pizza no soportado: {kind}")


class PizzaStore:
    def __init__(self, factory: SimplePizzaFactory) -> None:
        self.factory = factory

    def order_pizza(self, kind: str) -> Pizza:
        pizza = self.factory.create_pizza(kind)
        pizza.prepare()
        pizza.bake()
        pizza.box()
        return pizza

El cliente (PizzaStore) ya no depende de las clases concretas; toda la lógica de selección vive en el factory. La limitación es evidente: si necesitamos que distintas sucursales produzcan variantes propias de cada pizza, el if/elif termina explotando. Ese es exactamente el problema que resuelve Factory Method.

Info

El Simple Factory es útil como técnica de refactor cuando queremos aislar una serie de condicionales de creación, pero no ofrece extensibilidad vía herencia.

Factory Method

Note

El Factory Method define una interfaz para crear un objeto, pero delega en las subclases la decisión de qué clase concreta instanciar.

En lugar de tener una única clase factory que decide con condicionales, el Factory Method define un método abstracto de creación en la superclase. Cada subclase implementa ese método y devuelve el producto concreto que corresponda. La instanciación se difiere a las subclases.

classDiagram
    class Creator {
        <<abstract>>
        +factory_method() Product
        +operation()
    }
    class ConcreteCreatorA {
        +factory_method() Product
    }
    class ConcreteCreatorB {
        +factory_method() Product
    }
    class Product {
        <<abstract>>
    }
    class ConcreteProductA
    class ConcreteProductB

    Creator <|-- ConcreteCreatorA
    Creator <|-- ConcreteCreatorB
    Product <|-- ConcreteProductA
    Product <|-- ConcreteProductB
    ConcreteCreatorA ..> ConcreteProductA : crea
    ConcreteCreatorB ..> ConcreteProductB : crea

Consideremos un ejemplo canónico: una jerarquía de animales donde cada subclase decide qué instancia producir. La superclase define el flujo (show_animal) que consume el producto creado por el factory_method:

from abc import ABC, abstractmethod


class Animal(ABC):
    @abstractmethod
    def make_sound(self) -> str: ...

    @abstractmethod
    def create_animal(self) -> "Animal": ...

    def show_animal(self) -> None:
        animal = self.create_animal()
        print(f"El animal dice: {animal.make_sound()}")


class Dog(Animal):
    def make_sound(self) -> str:
        return "Guau!"

    def create_animal(self) -> Animal:
        return Dog()


class Cat(Animal):
    def make_sound(self) -> str:
        return "Miau!"

    def create_animal(self) -> Animal:
        return Cat()


Dog().show_animal()  # El animal dice: Guau!
Cat().show_animal()  # El animal dice: Miau!

El punto clave es que Animal.show_animal es agnóstico a la clase concreta: opera sobre la abstracción Animal y delega la instanciación a create_animal. Añadir un nuevo animal implica una nueva subclase, no modificar código existente — cumpliendo el principio abierto/cerrado.

Tip

Factory Method depende de herencia: cada subclase implementa el método de creación. Es ideal cuando la variación entre productos está atada a la variación de la clase creadora.

Abstract Factory

Mientras Factory Method crea un producto, el Abstract Factory crea familias completas de productos relacionados sin acoplar al cliente a las clases concretas de cada familia.

Un caso clásico: una aplicación que renderiza componentes de UI para distintos sistemas operativos. Botones y checkboxes de Windows deben usarse juntos, igual que los de macOS; mezclarlos rompe la coherencia visual. El Abstract Factory garantiza que solo se combinen componentes de la misma familia.

classDiagram
    class GUIFactory {
        <<abstract>>
        +create_button() Button
        +create_checkbox() Checkbox
    }
    class WindowsFactory
    class MacFactory
    class Button {
        <<abstract>>
        +render()
    }
    class Checkbox {
        <<abstract>>
        +render()
    }
    class WindowsButton
    class WindowsCheckbox
    class MacButton
    class MacCheckbox

    GUIFactory <|-- WindowsFactory
    GUIFactory <|-- MacFactory
    Button <|-- WindowsButton
    Button <|-- MacButton
    Checkbox <|-- WindowsCheckbox
    Checkbox <|-- MacCheckbox
    WindowsFactory ..> WindowsButton : crea
    WindowsFactory ..> WindowsCheckbox : crea
    MacFactory ..> MacButton : crea
    MacFactory ..> MacCheckbox : crea

Implementación en Python:

from abc import ABC, abstractmethod


class Button(ABC):
    @abstractmethod
    def render(self) -> None: ...


class Checkbox(ABC):
    @abstractmethod
    def render(self) -> None: ...


class WindowsButton(Button):
    def render(self) -> None:
        print("Renderizando botón de Windows")


class WindowsCheckbox(Checkbox):
    def render(self) -> None:
        print("Renderizando checkbox de Windows")


class MacButton(Button):
    def render(self) -> None:
        print("Renderizando botón de macOS")


class MacCheckbox(Checkbox):
    def render(self) -> None:
        print("Renderizando checkbox de macOS")


class GUIFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button: ...

    @abstractmethod
    def create_checkbox(self) -> Checkbox: ...


class WindowsFactory(GUIFactory):
    def create_button(self) -> Button:
        return WindowsButton()

    def create_checkbox(self) -> Checkbox:
        return WindowsCheckbox()


class MacFactory(GUIFactory):
    def create_button(self) -> Button:
        return MacButton()

    def create_checkbox(self) -> Checkbox:
        return MacCheckbox()


class Application:
    def __init__(self, factory: GUIFactory) -> None:
        self.factory = factory

    def create_ui(self) -> None:
        button = self.factory.create_button()
        checkbox = self.factory.create_checkbox()
        button.render()
        checkbox.render()


factory: GUIFactory = WindowsFactory()  # o MacFactory()
Application(factory).create_ui()

Application depende exclusivamente de GUIFactory, Button y Checkbox — todas abstracciones. Cambiar de familia consiste en inyectar una factory distinta; el resto del código permanece intacto.

Factory Method vs Abstract Factory

Aunque comparten espíritu, no son intercambiables:

  • Factory Method usa herencia: la subclase decide qué producto crear implementando un método.
  • Abstract Factory usa composición: el cliente recibe una factory y llama a varios métodos de creación sobre ella, obteniendo una familia de productos coherentes.
  • Factory Method escala en la dimensión de “un producto que varía”; Abstract Factory escala en la dimensión de “múltiples productos que varían de forma correlacionada”.

Static Factory

El Static Factory es una variante ligera que expone métodos de clase estáticos para construir instancias, en lugar de exponer constructores públicos. Es común en librerías del ecosistema Java y en APIs de Python que ofrecen métodos como datetime.now() o Path.home().

class Connection:
    def __init__(self, dsn: str) -> None:
        self._dsn = dsn

    @classmethod
    def from_env(cls) -> "Connection":
        import os
        return cls(dsn=os.environ["DATABASE_URL"])

    @classmethod
    def in_memory(cls) -> "Connection":
        return cls(dsn="sqlite://:memory:")

Ventajas: los métodos estáticos pueden tener nombres semánticos (from_env, in_memory) que documentan la intención mucho mejor que un constructor sobrecargado. Limitación: no permite polimorfismo — no es reemplazable en tiempo de ejecución como sí lo son Factory Method o Abstract Factory.

Principio de Inversión de Dependencias

El Principio de Inversión de Dependencias (DIP, la “D” de SOLID) enuncia dos reglas:

  1. Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones.
  2. Las abstracciones no deben depender de detalles. Los detalles deben depender de abstracciones.

Los patrones factory son una herramienta natural para aplicar DIP: al centralizar la creación en una abstracción, el módulo de alto nivel deja de instanciar clases concretas. Volviendo al ejemplo de la pizzería:

flowchart LR
    subgraph Antes["Sin DIP"]
        A1[PizzaStore] --> A2[CheesePizza]
        A1 --> A3[VeggiePizza]
        A1 --> A4[PepperoniPizza]
    end
    subgraph Despues["Con DIP + Factory"]
        B1[PizzaStore] --> B2[Pizza abstracta]
        B3[CheesePizza] --> B2
        B4[VeggiePizza] --> B2
        B5[PepperoniPizza] --> B2
    end

En “Antes”, el flujo de dependencia va de alto a bajo nivel: PizzaStore conoce todas las pizzas concretas. En “Después”, ambos dependen de la abstracción Pizza: la dependencia queda invertida.

Un ejemplo aislado en Python que muestra el patrón:

from abc import ABC, abstractmethod


class MessageSender(ABC):
    @abstractmethod
    def send(self, message: str) -> None: ...


class EmailSender(MessageSender):
    def send(self, message: str) -> None:
        print(f"Enviando email: {message}")


class SmsSender(MessageSender):
    def send(self, message: str) -> None:
        print(f"Enviando SMS: {message}")


class NotificationService:
    def __init__(self, sender: MessageSender) -> None:
        self._sender = sender

    def notify(self, message: str) -> None:
        self._sender.send(message)


NotificationService(EmailSender()).notify("Nuevo despliegue exitoso")

NotificationService no conoce EmailSender ni SmsSender; solo la abstracción MessageSender. Un factory puede alimentar esa dependencia según configuración, entorno o feature flags, sin tocar NotificationService.

Consideraciones

  • Todos los factories encapsulan la creación de objetos y reducen el acoplamiento a implementaciones concretas.
  • Simple Factory no es un patrón GoF; es un idiom útil para aislar condicionales de creación en un único lugar.
  • Factory Method depende de herencia: cada subclase implementa el método de creación y decide el producto concreto.
  • Abstract Factory depende de composición: el cliente recibe una factory y obtiene familias de productos coherentes.
  • Los tres refuerzan el Principio de Inversión de Dependencias al hacer que el código cliente dependa de abstracciones, no de tipos concretos.
  • El DIP es una guía transversal, no un patrón: aplica siempre que quieras evitar el acoplamiento con implementaciones específicas.
  • Factory es una técnica de diseño, no una clase específica. Su nombre puede variar (build, create, from_x), lo importante es la intención: encapsular la construcción de objetos.

Note

🚀 Elige Simple Factory para aislar condicionales, Factory Method cuando la variación está atada a la jerarquía del creador, y Abstract Factory cuando necesitas coherencia entre múltiples productos relacionados.

Continúa la serie

Si te interesa seguir explorando patrones de diseño, revisa los otros artículos de la serie:

Etiquetas :