Patrones creacionales - MiguelAngelMarinMejia1/ArquitecturaDeSoftware-Proyecto GitHub Wiki

Builder — ReservationBuilder

Archivo: reservation/domain/builders.py

Por qué la reserva y no otra entidad

Reservation es la entidad más compleja del sistema. Necesita ensamblar:

  • un Customer que puede ser nuevo o preexistente,
  • una o varias habitaciones (la multiplicidad 1..* del diagrama),
  • un rango de fechas,
  • una ocupación repartida entre adultos y niños,
  • un precio por noche congelado para cada habitación,
  • tres importes derivados (subtotal, impuestos, total),
  • un código de confirmación único.

Construirla con un constructor sería una llamada de once argumentos posicionales donde una combinación inválida solo se detecta después de que el objeto ya existe. El Builder resuelve las dos cosas: da nombre a cada parte y garantiza que no se puede producir una reserva inválida.

Interfaz fluida

draft = (
    ReservationBuilder(pricing_policy=self._pricing)
    .for_customer(customer)
    .add_rooms(rooms)
    .with_dates(data["check_in_date"], data["check_out_date"])
    .with_guests(data.get("num_adults"), data.get("num_children", 0))
    .with_special_requests(data.get("special_requests", ""))
    .with_confirmation_code(code)
    .build()
)

Cada método devuelve self, lo que permite encadenar sin obligar a un orden concreto: with_dates() antes o después de add_rooms() da el mismo resultado. El orden solo importa en build().

Las cinco invariantes

build() ejecuta las validaciones en orden de coste creciente: primero las que no tocan la base de datos, y solo al final la consulta de disponibilidad.

Invariante Excepción HTTP
Todos los campos obligatorios presentes IncompleteReservationDataError 400
Al menos una habitación (1..*) NoRoomsSelectedError 400
Salida posterior a entrada, y entrada no en el pasado InvalidDateRangeError 400
Huéspedes dentro de la capacidad combinada GuestCapacityExceededError 400
Ninguna habitación solapada RoomNotAvailableError 409

La decisión de diseño más importante: build() no persiste

def build(self) -> ReservationDraft:
    ...
    reservation = Reservation(...)      # instancia en memoria, NO .create()
    room_lines = [ReservationRoom(...) for room in self._rooms]
    return ReservationDraft(reservation=reservation, room_lines=room_lines, ...)

Devuelve un ReservationDraft sin guardar. Persistir es responsabilidad de ReservationService, dentro de su transacción.

Esto cumple SRP de forma estricta —el Builder construye y valida, nada más— y tiene una consecuencia práctica: permite construir una reserva solo para cotizarla, sin escribir en la base de datos. Si build() llamara a objects.create(), cada cotización dejaría basura persistida.

ReservationDraft agrupa la reserva con sus líneas de habitación porque juntas forman un agregado: guardar la reserva sin sus habitaciones dejaría el sistema en un estado imposible.

Inyección de colaboradores

def __init__(self, pricing_policy=None, availability_checker=None):
    self._pricing = pricing_policy or PricingPolicy()
    self._is_available = availability_checker or self._default_availability_checker

La política de precios y el comprobador de disponibilidad se inyectan. Una prueba unitaria puede pasar un comprobador falso y validar las reglas del Builder sin base de datos. Y cambiar la estrategia de precios no requiere editar el Builder (Open/Closed).


Factory — NotifierFactory y PaymentGatewayFactory

Archivo: reservation/infra/factories.py

Por qué Factory y no instanciación directa

El servicio necesita un notificador y una pasarela, pero elegir cuál es una decisión de configuración, no de negocio. Si ReservationService escribiera EmailNotifier(), quedaría atado a esa clase: no se podría probar sin un servidor SMTP, y añadir notificación por WhatsApp obligaría a editar el servicio.

Cada factoría tiene una sola responsabilidad: traducir un valor de configuración en el objeto correcto.

NotifierFactory

class NotifierFactory:
    _REGISTRY = {
        "CONSOLE": ConsoleNotifier,
        "EMAIL": EmailNotifier,
    }

    @classmethod
    def create(cls, notifier_type=None):
        key = (notifier_type or settings.NOTIFIER_TYPE).upper()
        return cls._REGISTRY.get(key, ConsoleNotifier)()

El registro como diccionario evita la cadena de if/elif que crecería con cada canal nuevo. Añadir notificación por SMS son dos líneas: la clase adaptadora y su entrada en el registro.

Ante un valor desconocido cae en ConsoleNotifier en lugar de fallar: quedarse sin notificar es peor que notificar por un canal de desarrollo.

PaymentGatewayFactory

@classmethod
def create(cls, gateway_type=None):
    key = (gateway_type or settings.PAYMENT_GATEWAY_TYPE).upper()
    gateway_class = cls._REGISTRY.get(key)
    if gateway_class is None:
        raise ValueError(f"Pasarela de pago no soportada: '{key}'.")
    return gateway_class()

Aquí la política ante un valor desconocido es la contraria: falla ruidosamente. Cobrar por una pasarela que no es la configurada sería un error con consecuencias financieras; es preferible que la aplicación no arranque.

Que dos factorías del mismo sistema tomen decisiones opuestas ante el mismo escenario no es una inconsistencia: refleja que el coste de equivocarse es distinto en cada caso.

Cómo lo consumen los servicios

class ReservationService:
    def __init__(self, notifier=None, ...):
        self._notifier = notifier or NotifierFactory.create()

El servicio depende de la abstracción BaseNotifier y recibe la implementación por constructor, con la factoría como valor por defecto. Esto es lo que la rúbrica llama servicios inyectables: en producción se usa el default; en una prueba se pasa un doble.

service = ReservationService(notifier=FakeNotifier())   # sin SMTP, sin configuración

Relación entre ambos patrones

Los dos resuelven problemas de creación, pero distintos:

Builder Factory
Problema Un objeto con muchas partes e invariantes Elegir entre varias implementaciones
Decide Cómo se ensambla Cuál se instancia
Varía por Los datos de cada reserva La configuración del entorno
Aplicado a Reservation (dominio) Notificador y pasarela (infraestructura)

Que uno viva en domain/ y el otro en infra/ no es casualidad: el Builder construye una entidad de negocio y el Factory resuelve dependencias externas.