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
# 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
# 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
# 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
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
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
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
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
# 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 implementa
calculate_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:
- Crear
PostgreSQLConnectionClase - 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:
- Reducir el acoplamiento: El cliente no necesita conocer los detalles de creación de los productos específicos
- Mejorar la mantenibilidad: La lógica de creación se gestiona de forma centralizada, fácil de modificar y ampliar
- Aumentar la flexibilidad: Se pueden agregar fácilmente nuevos tipos de productos
- 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