Aplicar patrón factory en Python permite encapsular la lógica de creación de objetos y tomar decisiones limpias y reutilizables. Aquí se muestra cómo elegir entre Stripe, offline y local según el PaymentData y su PaymentType, usando enum, match, static method y un protocol para el procesador. Además, se integra en un class method con tipado Self y manejo de errores con ValueError.
¿Qué problema resuelve el patrón factory en pagos?
El objetivo es centralizar la decisión de qué procesador instanciar según la información de pago. El PaymentData ahora incluye un tipo mediante un enum de Python con valores: offline u online. El tipo por defecto es online, lo que aporta contexto sobre cómo cobrar.
Si el tipo es offline, retorna el procesador OfflinePaymentProcessor.
Si el tipo es online y la moneda es USD, retorna StripePaymentProcessor.
Si el tipo es online y la moneda no es USD, retorna LocalPaymentProcessor.
Se define un caso por defecto que lanza ValueError para tipos no soportados.
Así, la lógica de decisión queda encapsulada y puede reutilizarse en todo el servicio.
¿Cómo implementar PaymentProcessorFactory con match en Python?
La clase PaymentProcessorFactory expone un static methodcreatePaymentProcessor que recibe paymentData y devuelve un PaymentProcessorProtocol. La decisión se implementa con el operador de pattern matchingmatch.
from commons import PaymentData, PaymentType
from processors import( PaymentProcessorProtocol, OfflinePaymentProcessor, StripePaymentProcessor, LocalPaymentProcessor,)classPaymentProcessorFactory:@staticmethoddefcreatePaymentProcessor(paymentData: PaymentData)-> PaymentProcessorProtocol:match paymentData.type:case PaymentType.OFFLINE:return OfflinePaymentProcessor()case PaymentType.ONLINE:match paymentData.currency:case"USD":return StripePaymentProcessor()case_:return LocalPaymentProcessor()case_:# Caso ejemplo para tipos no soportadosraise ValueError("Tipo de pago no soportado")
Puntos clave:
PaymentType es un enum con valores offline y online.
Se usa match con case y _ como caso por defecto.
El método retorna un PaymentProcessorProtocol, asegurando contrato de integración.
Se lanza ValueError para señalizar validaciones fallidas.
¿Cómo integrarlo en el servicio con createWithPaymentProcessor?
Se añade un class method que, a partir del paymentData, pide al factory el procesador adecuado y crea la instancia del servicio con los demás atributos que llegan como kwargs: notifier, customer validator, payment validator y logger.
from typing import Self
from factory import PaymentProcessorFactory
classPaymentService:@classmethoddefcreateWithPaymentProcessor(cls, paymentData: PaymentData,**kwargs)-> Self:try: processor = PaymentProcessorFactory.createPaymentProcessor(paymentData)return cls(payment_processor=processor,**kwargs)except ValueError as e:print("Error creando la clase")raise e
Se utiliza Self para indicar que retorna una instancia de la misma clase.
kwargs actúa como diccionario con los atributos restantes del servicio.
El try/except captura ValueError lanzada por el factory.
¿Cómo usarlo desde main con PaymentData de ejemplo?
El PaymentData incluye monto, fuente y moneda. Si la moneda es USD y el tipo por defecto es online, se selecciona Stripe.
from commons import PaymentData
paymentData = PaymentData( amount=100, source="stock visa", currency="USD",# type por defecto: online)service = PaymentService.createWithPaymentProcessor( paymentData, notifier=notifier, customer_validator=customer_validator, payment_validator=payment_validator, logger=logger,)
¿Qué habilidades y conceptos aplicas aquí?
Patrón factory: encapsular creación y aislar reglas de negocio.
Enums: PaymentType con offline y online.
Match/case en Python para decisiones claras y anidadas.
Protocolos: PaymentProcessorProtocol como contrato de integración.
Métodos estáticos y de clase: static method para crear, class method para orquestar.
Tipado avanzado: uso de Self y tipos explícitos.
Manejo de errores: ValueError para entradas no soportadas.
Composición con kwargs: pasar notifier, customer validator, payment validator y logger al constructor.
Depuración: uso de breakpoint para verificar el flujo y la instancia seleccionada.
¿De qué otra forma aplicarías patrón factory en este proyecto? Comparte tus ideas en comentarios.
El **Patrón Factory** es útil para centralizar la creación de objetos, especialmente cuando tienes varias clases que comparten una interfaz común o base y quieres que el cliente no se preocupe por los detalles de su creación. Implementaremos un ejemplo paso a paso para entenderlo.
---
### Ejemplo: Sistema de Vehículos
Queremos implementar un sistema para crear vehículos como autos, motocicletas y camiones. Cada uno tendrá características específicas, pero todos compartirán un comportamiento común: **mostrar su tipo de vehículo**.
---
### Paso 1: Crear la Interfaz Común
Definimos una clase base o interfaz que todas las clases de vehículos implementarán.
from abc import ABC, abstractmethod
classVehicle(ABC):  @abstractmethod  def get\_type(self) -> str:  """Devuelve el tipo de vehículo."""  pass
Si deseas agregar, por ejemplo, una bicicleta al sistema:
1. Crea una nueva clase concreta:
```python
class Bicycle(Vehicle):
def get_type(self) -> str:
return "Bicicleta"
```
2. Agrega su mapeo en el diccionario de la fábrica:
```python
_vehicle_classes = {
"car": Car,
"motorcycle": Motorcycle,
"truck": Truck,
"bicycle": Bicycle, # Nueva clase agregada
}
```
### Conclusión
El **Patrón Factory** simplifica la creación de objetos al delegar esta tarea a una clase específica. Es un patrón especialmente útil cuando se espera que el sistema crezca, ya que permite añadir nuevos tipos de objetos sin modificar el código del cliente, promoviendo el **Principio Abierto/Cerrado (OCP)**.
Este ejemplo muestra cómo aplicar el patrón de forma sencilla en Python, con la flexibilidad para adaptarlo a proyectos más complejos.
la clase explica que CreatePaymentProcessor del factory devuelve una instancia de PaymentProcessorProtocol. Esta instancia ya tiene definido el comportamiento para refund y recurring, por lo que sí se pueden ejecutar esas acciones.
match en Python (disponible desde la versión 3.10) es una potente estructura de control para la coincidencia de patrones(structural pattern matching), similar a un switch-case pero mucho más flexible, que permite evaluar una expresión contra múltiples patrones (valores, tipos, estructuras de datos) y ejecutar código si hay una coincidencia, ofreciendo una alternativa más legible a largas cadenas if-elif-else para comparar valores o estructuras complejas de forma declarativa.
Pregunta, entiendo que tambien es posible llamar al PaymentProcessorFactory desde el main sin la necesidad de crear el classmethod. Cual es la ventaja/desventaja de tener la llamada al factory desde dentro de la clase PaymentService vs llamar al PaymentProcessorFactory antes de instanciar la clase PaymentService y mandar el ProtocolProcessor como paramtro de la clase como se hacia antes?
no se si se entendio la pregunta? Gracias.
La ventaja de llamar al Factory dentro de PaymentService mediante un classmethod es que el servicio encapsula la lógica de decidir qué procesador usar, siguiendo el Principio de Responsabilidad Única. Si lo pasaras como parámetro, la decisión recaería en el código cliente.
Hola, estoy intentando ejecutar un proyecto desde los recursos que tengo, pero al correr el archivo main.py, obtengo el siguiente error en todos los imports:
Exception has occurred: ImportError
attempted relative import with no known parent package
File "/root/Python_SourceCode/Python_examples/Python_Design_Patterns_and_SOLID/src/payment_service/main.py", line 1, in <module>
from .loggers import TransactionLogger
ImportError: attempted relative import with no known parent package
¿Qué podría estar causando este error y cómo lo puedo solucionar? ¡Gracias de antemano!
Hola, me pasaba lo mismo con Listener, así que antes de importar la librería tuve que usar esta linea:
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
Debajo puse a la clase a importar y ya me funcionó bien.
Aunque no es la mejor forma de correrlo, lo reconozco, ya que todo está en orden. También debo investigar por qué jeheh
Usar @staticmethod en Python es útil cuando necesitas definir un método que no requiere acceso a los atributos o métodos de instancia o de clase. Al utilizarlo, estás indicando que este método pertenece a la clase y puede ser llamado sin instanciar un objeto de esa clase. Esto mejora la claridad del código y la organización, ya que separa la lógica que no depende del estado de la instancia.
En el contexto del patrón Factory mencionado, un método estático puede encapsular la lógica de creación de instancias sin necesidad de modificar el estado de la clase.
PaymentProcessorFactory es una clase con un método estático para encapsular la lógica compleja de decisión sobre qué procesador de pago instanciar, basándose en PaymentData. Si fuera solo una función, perdería la estructura y organización que aporta el patrón Factory. La clase lo mantiene todo centralizado y reutilizable.
¿Por qué no llamó directamente al método factory directamente desde el main, usando el constructor que ya tenía la clase Payment Service en lugar de crear ese método @classmethod ?
el classmethod se usa en PaymentService para encapsular la lógica de decisión sobre qué PaymentProcessor instanciar, basándose en PaymentData. Esto aplica el patrón Factory, centralizando la creación de objetos complejos y haciendo el código más mantenible.