Patrón de fábrica en Python

El patrón de fábrica es un patrón de diseño creacional que proporciona una forma óptima de crear objetos.

Imagina que vas a un restaurante a pedir comida: no necesitas saber cómo la cocina prepara los alimentos, solo tienes que decirle al camarero lo que quieres y la cocina lo preparará por ti.

El patrón de fábrica es esa "cocina": se encarga de crear objetos, y tú solo necesitas decirle qué tipo de objeto deseas.

¿Por qué necesitamos el patrón de fábrica?

En programación, a menudo necesitamos crear objetos. Si en el código usamos directamentenewla palabra clave o el constructor de la clase, causará:

  • Alto acoplamiento del código: el código que crea objetos está estrechamente vinculado a la clase concreta
  • Mantenimiento difícil: cuando necesitas modificar o agregar nuevos tipos de objetos, debes modificar múltiples partes del código
  • Viola el principio de abierto/cerrado: se rompe el principio de abierto para extensión, cerrado para modificación

El patrón de fábrica resuelve estos problemas encapsulando el proceso de creación de objetos.


Tres tipos de patrón de fábrica

El patrón de fábrica se divide principalmente en tres tipos. Entendámoslos mediante ejemplos concretos.

1. Patrón de fábrica simple

El patrón de fábrica simple es el más básico; utiliza una clase de fábrica para crear diferentes tipos de objetos.

Estructura básica

Ejemplo

from abc import ABC, abstractmethod

# Interfaz de producto
class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

# Producto concreto
class Dog(Animal):
    def speak(self):
        return "¡Guau!"

class Cat(Animal):
    def speak(self):
        return "¡Miau!"

class Duck(Animal):
    def speak(self):
        return "¡Cuac!"

# Fábrica simple
class AnimalFactory:
    @staticmethod
    def create_animal(animal_type):
        if animal_type == "dog":
            return Dog()
        elif animal_type == "cat":
            return Cat()
        elif animal_type == "duck":
            return Duck()
        else:
            raise ValueError(f"Tipo de animal desconocido: {animal_type}")

# Ejemplo de uso
def test_simple_factory():
    factory = AnimalFactory()
   
    dog = factory.create_animal("dog")
    cat = factory.create_animal("cat")
    duck = factory.create_animal("duck")
   
    print(dog.speak())  # Salida: ¡Guau!
    print(cat.speak())  # Salida: ¡Miau!
    print(duck.speak()) # Salida: ¡Cuac!

if __name__ == "__main__":
    test_simple_factory()

Ventajas y desventajas

Ventajas:

  • Desacopla al cliente de las clases de producto concretas
  • Separación de responsabilidades, fácil de mantener

Desventajas:

  • Agregar un nuevo producto requiere modificar la clase de fábrica, violando el principio de abierto/cerrado
  • La clase de fábrica tiene demasiadas responsabilidades, no cumple el principio de responsabilidad única

2. Patrón de método de fábrica

El patrón de método de fábrica resuelve los problemas del patrón de fábrica simple al permitir que las subclases decidan qué objeto crear.

Estructura básica

Ejemplo

from abc import ABC, abstractmethod

# Interfaz de producto
class Button(ABC):
    @abstractmethod
    def render(self):
        pass
   
    @abstractmethod
    def onClick(self):
        pass

# Producto concreto
class WindowsButton(Button):
    def render(self):
        return "Renderizar botón de estilo Windows"
   
    def onClick(self):
        return "Botón de Windows presionado"

class MacButton(Button):
    def render(self):
        return "Renderizar botón de estilo Mac"
   
    def onClick(self):
        return "Botón de Mac presionado"

# Clase abstracta de creador
class Dialog(ABC):
    @abstractmethod
    def createButton(self) -> Button:
        pass
   
    def render(self):
        # Llamar al método de fábrica para crear el producto
        button = self.createButton()
        result = button.render()
        return result

# Creador concreto
class WindowsDialog(Dialog):
    def createButton(self) -> Button:
        return WindowsButton()

class MacDialog(Dialog):
    def createButton(self) -> Button:
        return MacButton()

# Ejemplo de uso
def test_factory_method():
    # Seleccionar la fábrica concreta según la configuración
    config = "windows"  # Se puede leer desde el archivo de configuración
   
    if config == "windows":
        dialog = WindowsDialog()
    else:
        dialog = MacDialog()
   
    result = dialog.render()
    print(result)

if __name__ == "__main__":
    test_factory_method()

Flujo del patrón de método de fábrica

3. Patrón de fábrica abstracta

El patrón de fábrica abstracta proporciona una interfaz para crear familias de objetos relacionados o dependientes sin especificar sus clases concretas.

Estructura básica

Ejemplo

from abc import ABC, abstractmethod

# Producto abstracto A
class Button(ABC):
    @abstractmethod
    def paint(self):
        pass

# Producto abstracto B
class Checkbox(ABC):
    @abstractmethod
    def paint(self):
        pass

# Producto concreto A1
class WindowsButton(Button):
    def paint(self):
        return "Renderizar botón de Windows"

# Producto concreto A2
class MacButton(Button):
    def paint(self):
        return "Renderizar botón de Mac"

# Producto concreto B1
class WindowsCheckbox(Checkbox):
    def paint(self):
        return "Renderizar casilla de verificación de Windows"

# Producto concreto B2
class MacCheckbox(Checkbox):
    def paint(self):
        return "Renderizar casilla de verificación de Mac"

# Fábrica abstracta
class GUIFactory(ABC):
    @abstractmethod
    def createButton(self) -> Button:
        pass
   
    @abstractmethod
    def createCheckbox(self) -> Checkbox:
        pass

# Fábrica concreta 1
class WindowsFactory(GUIFactory):
    def createButton(self) -> Button:
        return WindowsButton()
   
    def createCheckbox(self) -> Checkbox:
        return WindowsCheckbox()

# Fábrica concreta 2
class MacFactory(GUIFactory):
    def createButton(self) -> Button:
        return MacButton()
   
    def createCheckbox(self) -> Checkbox:
        return MacCheckbox()

# Código del cliente
class Application:
    def __init__(self, factory: GUIFactory):
        self.factory = factory
        self.button = None
        self.checkbox = None
   
    def createUI(self):
        self.button = self.factory.createButton()
        self.checkbox = self.factory.createCheckbox()
   
    def paint(self):
        result = []
        if self.button:
            result.append(self.button.paint())
        if self.checkbox:
            result.append(self.checkbox.paint())
        return "\n".join(result)

# Ejemplo de uso
def test_abstract_factory():
    # Seleccionar la fábrica según el tipo de sistema
    system_type = "windows"  # Se puede detectar automáticamente o leer desde la configuración
   
    if system_type == "windows":
        factory = WindowsFactory()
    else:
        factory = MacFactory()
   
    app = Application(factory)
    app.createUI()
    print(app.paint())

if __name__ == "__main__":
    test_abstract_factory()

Comparación de los tres patrones de fábrica

Característica Patrón de fábrica simple Patrón de método de fábrica Patrón de fábrica abstracta
Complejidad bajo Medio Alto
Extensibilidad Malo Bien Muy buena
Escenarios aplicables Pocos tipos de objetos Familia de productos única Múltiples familias de productos relacionadas
Principio de abierto/cerrado Viola Cumple Cumple
Dependencia Depende de clases concretas Depende de clases abstractas Depende de interfaces abstractas

Escenarios de aplicación práctica

Escenario 1: Fábrica de conexiones de base de datos

Ejemplo

from abc import ABC, abstractmethod
import sqlite3
import mysql.connector

# Interfaz de conexión de base de datos
class DatabaseConnection(ABC):
    @abstractmethod
    def connect(self):
        pass
   
    @abstractmethod
    def execute(self, query):
        pass

# Conexión de base de datos concreta
class SQLiteConnection(DatabaseConnection):
    def __init__(self, db_path):
        self.db_path = db_path
        self.connection = None
   
    def connect(self):
        self.connection = sqlite3.connect(self.db_path)
        return self.connection
   
    def execute(self, query):
        if self.connection:
            cursor = self.connection.cursor()
            cursor.execute(query)
            return cursor.fetchall()

class MySQLConnection(DatabaseConnection):
    def __init__(self, host, user, password, database):
        self.host = host
        self.user = user
        self.password = password
        self.database = database
        self.connection = None
   
    def connect(self):
        self.connection = mysql.connector.connect(
            host=self.host,
            user=self.user,
            password=self.password,
            database=self.database
        )
        return self.connection
   
    def execute(self, query):
        if self.connection:
            cursor = self.connection.cursor()
            cursor.execute(query)
            return cursor.fetchall()

# Fábrica de bases de datos
class DatabaseFactory:
    @staticmethod
    def create_connection(db_type, **kwargs):
        if db_type == "sqlite":
            return SQLiteConnection(**kwargs)
        elif db_type == "mysql":
            return MySQLConnection(**kwargs)
        else:
            raise ValueError(f"Tipo de base de datos no soportado: {db_type}")

# Ejemplo de uso
def test_database_factory():
    # Crear conexión SQLite
    sqlite_conn = DatabaseFactory.create_connection(
        "sqlite",
        db_path="example.db"
    )
    sqlite_conn.connect()
   
    # Crear conexión MySQL
    mysql_conn = DatabaseFactory.create_connection(
        "mysql",
        host="localhost",
        user="root",
        password="password",
        database="test"
    )
    mysql_conn.connect()
   
    print("¡Conexión de base de datos creada con éxito!")

if __name__ == "__main__":
    test_database_factory()

Escenario 2: Fábrica de registradores de logs

Ejemplo

import logging
from abc import ABC, abstractmethod
import sys

# Interfaz de registrador de logs
class Logger(ABC):
    @abstractmethod
    def info(self, message):
        pass
   
    @abstractmethod
    def error(self, message):
        pass
   
    @abstractmethod
    def debug(self, message):
        pass

# Registrador de logs de consola
class ConsoleLogger(Logger):
    def info(self, message):
        print(f"INFO: {message}")
   
    def error(self, message):
        print(f"ERROR: {message}", file=sys.stderr)
   
    def debug(self, message):
        print(f"DEBUG: {message}")

# Registrador de logs de archivo
class FileLogger(Logger):
    def __init__(self, filename):
        self.filename = filename
   
    def info(self, message):
        with open(self.filename, 'a') as f:
            f.write(f"INFO: {message}\n")
   
    def error(self, message):
        with open(self.filename, 'a') as f:
            f.write(f"ERROR: {message}\n")
   
    def debug(self, message):
        with open(self.filename, 'a') as f:
            f.write(f"DEBUG: {message}\n")

# Fábrica de logs
class LoggerFactory:
    @staticmethod
    def get_logger(logger_type, **kwargs):
        if logger_type == "console":
            return ConsoleLogger()
        elif logger_type == "file":
            return FileLogger(**kwargs)
        else:
            raise ValueError(f"Tipo de log no soportado: {logger_type}")

# Ejemplo de uso
def test_logger_factory():
    # Crear registrador de consola
    console_logger = LoggerFactory.get_logger("console")
    console_logger.info("Este es un mensaje informativo")
    console_logger.error("Este es un mensaje de error")
   
    # Crear registrador de archivo
    file_logger = LoggerFactory.get_logger("file", filename="app.log")
    file_logger.info("Información registrada en archivo")
    file_logger.debug("Información de depuración")

if __name__ == "__main__":
    test_logger_factory()

Mejores prácticas y precauciones

1. ¿Cuándo usar el patrón de fábrica?

Situaciones adecuadas para usar el patrón de fábrica:

  • El proceso de creación del objeto es complejo
  • Se necesitan crear diferentes objetos según distintas condiciones
  • Se desea separar la creación de objetos de su uso
  • El sistema necesita admitir múltiples tipos de productos

Situaciones no adecuadas:

  • El proceso de creación del objeto es muy simple, basta con usar el constructor directamente
  • Hay pocos tipos de productos y es poco probable que se amplíen

2. Errores comunes y cómo evitarlos

Error 1: Diseño excesivo

Ejemplo

# No recomendado: usar una fábrica compleja para un caso simple
class SimpleObject:
    def __init__(self, name):
        self.name = name

# Fábrica con diseño excesivo
class SimpleObjectFactory:
    @staticmethod
    def create_simple_object(name):
        return SimpleObject(name)

# Recomendado: crear directamente
obj = SimpleObject("test")

Error 2: La clase de fábrica tiene demasiadas responsabilidades

Ejemplo

# No recomendado: una fábrica que hace demasiadas cosas
class GodFactory:
    def create_user(self): ...
    def create_order(self): ...
    def create_product(self): ...
    def send_email(self): ...  # ¡Esto no es crear objetos!

# Recomendado: separar según responsabilidades
class UserFactory: ...
class OrderFactory: ...
class ProductFactory: ...

3. Combinación con inyección de dependencias

El patrón de fábrica se usa a menudo junto con la inyección de dependencias (DI):

Ejemplo

from abc import ABC, abstractmethod

# Interfaz de servicio
class NotificationService(ABC):
    @abstractmethod
    def send(self, message):
        pass

# Servicio concreto
class EmailService(NotificationService):
    def send(self, message):
        return f"Enviar correo: {message}"

class SMSService(NotificationService):
    def send(self, message):
        return f"Enviar SMS: {message}"

# Fábrica
class NotificationFactory:
    @staticmethod
    def create_service(service_type):
        if service_type == "email":
            return EmailService()
        elif service_type == "sms":
            return SMSService()
        else:
            raise ValueError(f"Tipo de servicio desconocido: {service_type}")

# Clase que usa inyección de dependencias
class OrderProcessor:
    def __init__(self, notification_service: NotificationService):
        self.notification_service = notification_service
   
    def process_order(self, order):
        # Lógica de procesamiento de pedidos
        result = self.notification_service.send("Pedido procesado")
        return result

# Uso
def main():
    # Crear el servicio a través de la fábrica
    notification_service = NotificationFactory.create_service("email")
   
    # Inyectar dependencia
    processor = OrderProcessor(notification_service)
    result = processor.process_order({"id": 1})
    print(result)

if __name__ == "__main__":
    main()

Ejercicios

Ejercicio 1: Implementar una fábrica de formas

Crea una fábrica de formas que admita la creación de círculos (Circle), rectángulos (Rectangle) y triángulos (Triangle). Cada forma debe tener métodos para calcular el área y el perímetro.

Requisitos:

  • Usar el patrón de método de fábrica
  • Cada clase de forma implementacalculate_area()ycalculate_perimeter()métodos
  • Proporcionar ejemplos de uso

Ejercicio 2: Ampliar la fábrica de bases de datos

Basándose en el ejemplo anterior de la fábrica de bases de datos, agregar soporte para la base de datos PostgreSQL.

Requisitos:

  • CrearPostgreSQLConnectionClase
  • Modificar la fábrica para admitir el nuevo tipo de base de datos
  • Asegurarse de no romper el código existente

Ejercicio 3: Fábrica basada en configuración

Crear un sistema que pueda seleccionar dinámicamente una fábrica según los archivos de configuración.

Requisitos:

  • Leer la configuración desde archivos JSON o YAML
  • Crear los objetos correspondientes según la configuración
  • Soporte para actualización en caliente de la configuración

Resumen

El patrón de fábrica es un patrón de creación muy importante en el diseño de Python. Al encapsular el proceso de creación de objetos, proporciona los siguientes beneficios:

  1. Reducir el acoplamiento: El cliente no necesita conocer los detalles de creación de los productos específicos
  2. Mejorar la mantenibilidad: La lógica de creación se gestiona de forma centralizada, fácil de modificar y ampliar
  3. Aumentar la flexibilidad: Se pueden agregar fácilmente nuevos tipos de productos
  4. Promover la reutilización de código: La lógica de creación se puede reutilizar en múltiples lugares

Recuerda elegir el tipo de patrón de fábrica adecuado:

  • Fábrica simple: Adecuado para escenarios con pocos tipos de productos y que no cambian mucho
  • Método de fábrica: Adecuado para escenarios donde se necesita ampliar familias de productos
  • Fábrica abstracta: Adecuado para escenarios complejos donde se necesita crear familias de productos relacionadas

¡Usando el patrón de fábrica de manera razonable, puedes escribir código Python más flexible, mantenible y extensible!

Otras extensiones