Curso de Patrones de Diseño y SOLID en Python

Builder Pattern aplicado a un servicio de pagos

Curso de Patrones de Diseño y SOLID en Python

Contenido del curso

Principios SOLID

Patrones de Diseño

Builder Pattern aplicado a un servicio de pagos

Resumen

Construir un servicio con muchas dependencias puede volverse caótico si intentas instanciar todo de golpe. El patrón Builder en Python resuelve ese problema separando la construcción paso a paso, y aquí verás cómo aplicarlo a un payment service real, combinándolo con el patrón Factory.

Esta guía es para ti si estás aprendiendo patrones de diseño aplicados a servicios con múltiples validadores, procesadores y notificadores, y quieres ver cómo orquestar todo sin acoplar tu código.

¿Cuándo conviene usar el patrón Builder?

El Builder brilla cuando una clase necesita muchas configuraciones o pasos previos antes de instanciarse. En el caso del payment service, hay validadores, procesadores, notificadores y logger involucrados, así que tiene sentido construirlo por partes [00:09].

¿Qué es el patrón Builder? Es un patrón creacional que separa la construcción de un objeto complejo de su representación, permitiendo crear distintas variantes con los mismos pasos.

¿Cómo se estructura la clase PaymentServiceBuilder?

El primer paso es crear un archivo Builder.py con una clase PaymentServiceBuilder que tenga los mismos atributos que la clase final, pero con una particularidad clave: todos los atributos deben ser opcionales y con valor por defecto None [00:33]. Esto permite ir llenándolos progresivamente.

La clase se marca también como data class para simplificar la definición. Después se importan los protocolos y clases que cada atributo va a recibir, eliminando los que no se usen.

¿Cómo definir los métodos set del builder?

El segundo paso es crear un método set por cada atributo. Aquí conviene separar los atributos en dos grupos según su complejidad [01:30].

Setters simples sin lógica de validación

Algunos componentes no dependen de abstracciones y se pueden instanciar directamente:

  • set_logger: instancia un TransactionLogger y lo asigna a self.logger.
  • set_payment_validator: instancia PaymentDataValidator.
  • set_customer_validator: instancia CustomerValidator.

Cada método recibe self, asigna el atributo y termina con return self. Ese retorno de self es lo que habilita el method chaining, es decir, encadenar llamadas como builder.set_logger().set_payment_validator() [02:14].

Setters que dependen de lógica externa

El set_payment_processor se apoya en un factory creado previamente, que decide cuál es el mejor procesador según el tipo de pago [03:05]. Recibe un PaymentData adicional y llama a PaymentProcessorFactory.create_payment_processor(payment_data).

Aquí ocurre algo interesante: estás mezclando dos patrones de diseño. El Factory decide la mejor implementación para el contexto, y el Builder orquesta la construcción dinámica del servicio completo [03:55].

El set_notifier recibe un CustomerData y elige notificador con cláusulas if:

  • Si el contact_info tiene email, usa EmailNotifier.
  • Si tiene número de teléfono, usa SMSNotifier con un gateway personalizado.
  • Si no tiene ninguno, lanza raise ValueError("No se puede seleccionar clase de notificación") [05:12].

Una buena práctica que se sugiere: crear también un factory para el notificador, en lugar de resolverlo con if dentro del builder.

¿Para qué sirve el método build y cómo validar dependencias?

El tercer paso es el método build, responsable de validar que toda la información necesaria está presente antes de instanciar la clase final [06:14].

¿Qué hace el método build en el patrón Builder? Verifica que los atributos requeridos no sean None y devuelve la instancia final del objeto. Si falta algo, lanza una excepción clara.

La validación se hace con all() sobre una lista de los atributos obligatorios: payment_processor, notifier, customer_validator, payment_validator y logger. Si alguno es None, all() retorna False y entra a la rama de error.

Cómo notificar qué dependencia falta

En vez de fallar con un mensaje genérico, el builder construye una lista missing iterando sobre tuplas (name, value) de cada atributo requerido. Si el value es None, agrega el name a la lista. Al final lanza raise ValueError(f"Missing dependencies: {missing}") [07:46].

Esto evita que el error explote sin contexto y te dice exactamente qué setter olvidaste llamar.

Una vez validado todo, el método retorna PaymentService(...) pasándole cada atributo desde self. Si tu editor marca una advertencia porque cree que un atributo puede ser None, puedes ignorarla con seguridad: la validación previa ya garantiza que no lo es [09:08].

¿Cómo se usa el builder en el código cliente?

La instanciación queda mucho más limpia. Primero creas el builder, encadenas los setters y terminas con build():

python from builder import PaymentServiceBuilder

builder = PaymentServiceBuilder() service = (builder .set_logger() .set_payment_validator() .set_customer_validator() .set_payment_processor(payment_data) .set_notifier(customer_data) .build())

Si quitas el .build() final, lo que tienes es una instancia del builder, no del servicio. Solo al invocar build() obtienes la instancia tipada como PaymentService [10:34].

Qué pasa cuando falta una dependencia

Si omites, por ejemplo, set_payment_validator y ejecutas build(), el flujo entra a la validación, detecta que payment_validator está en None, lo agrega a missing y lanza la excepción indicando que la dependencia faltante es el payment validator [11:25]. Diagnóstico inmediato, sin adivinar.

Un reto que puedes intentar: agrega un método al builder que decore el comportamiento original del payment service usando los decorators vistos en clases anteriores. ¿Cómo lo resolverías tú? Déjalo en los comentarios.