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
Customerque 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.