← Volver al listado de tecnologías
Capítulo 1: Lenguaje Ubicuo
Capítulo 1: Lenguaje Ubicuo
¿Qué es el Lenguaje Ubicuo?
El Lenguaje Ubicuo (Ubiquitous Language) es el corazón de DDD. Es un vocabulario compartido y riguroso que:
- Emerge de las conversaciones entre desarrolladores y expertos del dominio
- Se usa consistentemente en código, documentación y conversaciones
- Evoluciona conforme el equipo comprende mejor el dominio
- Elimina la traducción mental entre “lenguaje técnico” y “lenguaje de negocio”
“El lenguaje ubicuo es el vehículo mediante el cual el conocimiento del dominio fluye hacia el modelo y el modelo hacia el código.” — Eric Evans
El Problema: La Brecha de Comunicación
┌─────────────────┐ ┌─────────────────┐
│ Experto del │ "Confirmar el │ Desarrollador │
│ Dominio │ pedido" │ │
│ │ ─────────────────▶│ "¿Qué método │
│ "El cliente │ │ llamo?" │
│ activa el │ Traducción │ │
│ pedido" │ mental │ setStatus(2)? │
└─────────────────┘ └─────────────────┘
│ │
│ RESULTADO: Bugs, │
└──────── malentendidos, ◀─────────────┘
código críptico
Código SIN Lenguaje Ubicuo
# MAL: Código técnico sin significado de negocio
class OrderManager:
def process(self, order_id: int, action: str, flag: bool) -> dict:
record = self.db.find(order_id)
if action == "confirm" and flag:
record["status"] = 2
record["ts"] = time.time()
self._send_notification(record["user_id"], "ORD_CONF")
return record
def update_status(self, order_id: int, status_code: int) -> None:
# ¿Qué significa status_code 3? ¿Y 4?
self.db.update(order_id, {"status": status_code})
Problemas:
status = 2no comunica nadaflages ambiguoprocesses genérico- Requiere documentación externa para entender
Código CON Lenguaje Ubicuo
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Optional
class EstadoPedido(Enum):
"""Estados posibles de un pedido según el flujo de negocio"""
BORRADOR = "borrador"
PENDIENTE_PAGO = "pendiente_pago"
PAGADO = "pagado"
CONFIRMADO = "confirmado"
EN_PREPARACION = "en_preparacion"
ENVIADO = "enviado"
ENTREGADO = "entregado"
CANCELADO = "cancelado"
@dataclass
class Pedido:
id: str
cliente: "Cliente"
lineas: list["LineaPedido"]
estado: EstadoPedido
fecha_creacion: datetime
fecha_confirmacion: Optional[datetime] = None
def confirmar(self) -> None:
"""
Confirma el pedido validando las reglas de negocio.
Un pedido solo puede confirmarse si está pagado y tiene items.
"""
if not self.puede_confirmarse():
raise PedidoNoConfirmableError(
f"Pedido {self.id} no cumple requisitos para confirmación"
)
self.estado = EstadoPedido.CONFIRMADO
self.fecha_confirmacion = datetime.now()
def puede_confirmarse(self) -> bool:
"""Verifica si el pedido cumple las reglas para ser confirmado"""
return (
self.estado == EstadoPedido.PAGADO
and self.tiene_lineas()
and self.cliente.esta_activo()
and not self.cliente.tiene_deuda_pendiente()
)
def tiene_lineas(self) -> bool:
return len(self.lineas) > 0
def cancelar(self, motivo: "MotivoCancelacion") -> None:
"""Cancela el pedido registrando el motivo"""
if self.estado in (EstadoPedido.ENVIADO, EstadoPedido.ENTREGADO):
raise PedidoNoCancelableError("No se puede cancelar pedido ya enviado")
self.estado = EstadoPedido.CANCELADO
self.motivo_cancelacion = motivo
Construyendo el Glosario del Dominio
El glosario es un documento vivo que mapea términos de negocio a código:
| Término de Negocio | En Código | Descripción | Reglas |
|---|---|---|---|
| Pedido | Pedido | Solicitud de compra | Debe tener al menos 1 línea |
| Confirmar pedido | pedido.confirmar() | Validar y aceptar | Solo si está pagado |
| Cliente activo | cliente.esta_activo() | Cuenta habilitada | Sin bloqueos ni deudas |
| Línea de pedido | LineaPedido | Item individual | Cantidad > 0 |
| Preparar envío | pedido.iniciar_preparacion() | Comenzar picking | Solo confirmados |
Proceso para Descubrir el Lenguaje
1. Event Storming
Técnica colaborativa para descubrir el dominio:
┌──────────────────────────────────────────────────────────────┐
│ EVENT STORMING │
├──────────────────────────────────────────────────────────────┤
│ 🟧 Evento: PedidoCreado │
│ 🟦 Comando: CrearPedido │
│ 🟨 Actor: Cliente │
│ 🟩 Agregado: Pedido │
│ 🟪 Política: NotificarVendedor │
│ 🟥 Problema: ¿Qué pasa si el stock es insuficiente? │
└──────────────────────────────────────────────────────────────┘
2. Conversaciones Estructuradas
# Antes de la conversación:
def process_order(order_id, status):
pass
# Después de hablar con expertos:
# "Cuando el cliente CONFIRMA un pedido, verificamos que esté PAGADO
# y que el cliente esté ACTIVO. Si todo está bien, el pedido pasa
# a estado CONFIRMADO y notificamos al almacén para PREPARAR el envío."
def confirmar_pedido(pedido_id: str) -> Pedido:
pedido = repositorio.obtener(pedido_id)
pedido.confirmar()
notificador.notificar_almacen(pedido)
return pedido
Implementación Práctica: Sistema de Biblioteca
from dataclasses import dataclass, field
from datetime import date, timedelta
from enum import Enum
from typing import Optional
from uuid import UUID, uuid4
class EstadoPrestamo(Enum):
ACTIVO = "activo"
DEVUELTO = "devuelto"
VENCIDO = "vencido"
class TipoSocio(Enum):
REGULAR = "regular"
ESTUDIANTE = "estudiante"
PROFESOR = "profesor"
@dataclass
class Socio:
id: UUID
nombre: str
tipo: TipoSocio
prestamos_activos: list["Prestamo"] = field(default_factory=list)
sanciones: list["Sancion"] = field(default_factory=list)
def puede_tomar_prestamo(self) -> bool:
"""Un socio puede tomar préstamos si no está sancionado
y no excede su límite"""
return (
not self.esta_sancionado()
and len(self.prestamos_activos) < self.limite_prestamos()
)
def esta_sancionado(self) -> bool:
return any(s.esta_vigente() for s in self.sanciones)
def limite_prestamos(self) -> int:
limites = {
TipoSocio.REGULAR: 3,
TipoSocio.ESTUDIANTE: 5,
TipoSocio.PROFESOR: 10,
}
return limites[self.tipo]
def dias_prestamo(self) -> int:
"""Días permitidos según tipo de socio"""
dias = {
TipoSocio.REGULAR: 14,
TipoSocio.ESTUDIANTE: 21,
TipoSocio.PROFESOR: 30,
}
return dias[self.tipo]
@dataclass
class Libro:
isbn: str
titulo: str
autor: str
esta_disponible: bool = True
def prestar(self) -> None:
if not self.esta_disponible:
raise LibroNoDisponibleError(self.isbn)
self.esta_disponible = False
def devolver(self) -> None:
self.esta_disponible = True
@dataclass
class Prestamo:
id: UUID = field(default_factory=uuid4)
socio: Socio = None
libro: Libro = None
fecha_prestamo: date = None
fecha_devolucion_esperada: date = None
fecha_devolucion_real: Optional[date] = None
estado: EstadoPrestamo = EstadoPrestamo.ACTIVO
@classmethod
def crear(cls, socio: Socio, libro: Libro) -> "Prestamo":
"""Factory method que aplica reglas de negocio"""
if not socio.puede_tomar_prestamo():
raise SocioNoPuedePrestamo(socio.id)
libro.prestar()
prestamo = cls(
socio=socio,
libro=libro,
fecha_prestamo=date.today(),
fecha_devolucion_esperada=date.today() + timedelta(days=socio.dias_prestamo()),
)
socio.prestamos_activos.append(prestamo)
return prestamo
def devolver(self) -> Optional["Sancion"]:
"""Procesa la devolución y aplica sanción si hay retraso"""
self.fecha_devolucion_real = date.today()
self.libro.devolver()
self.socio.prestamos_activos.remove(self)
if self.esta_vencido():
dias_retraso = (self.fecha_devolucion_real - self.fecha_devolucion_esperada).days
sancion = Sancion.por_retraso(self.socio, dias_retraso)
self.socio.sanciones.append(sancion)
self.estado = EstadoPrestamo.VENCIDO
return sancion
self.estado = EstadoPrestamo.DEVUELTO
return None
def esta_vencido(self) -> bool:
fecha_comparacion = self.fecha_devolucion_real or date.today()
return fecha_comparacion > self.fecha_devolucion_esperada
Antipatrones a Evitar
1. Traducción Mental
# MAL: Requiere traducción mental
if order.flag == 2: # ¿Qué es flag 2?
order.status = 3 # ¿Y status 3?
# BIEN: Auto-documentado
if pedido.estado == EstadoPedido.PAGADO:
pedido.confirmar()
2. Nombres Genéricos
# MAL: Nombres que no dicen nada
class Manager:
def process(self, data): pass
def handle(self, item): pass
def execute(self, request): pass
# BIEN: Nombres del dominio
class GestorPrestamos:
def registrar_prestamo(self, socio: Socio, libro: Libro): pass
def procesar_devolucion(self, prestamo: Prestamo): pass
def renovar_prestamo(self, prestamo: Prestamo): pass
3. Abreviaciones Oscuras
# MAL
usr_act = check_usr_stat(usr_id)
proc_ord(ord_id, usr_act)
# BIEN
usuario_activo = verificar_estado_usuario(usuario_id)
procesar_pedido(pedido_id, usuario_activo)
Checklist de Validación
- ¿Los nombres de clases reflejan conceptos del negocio?
- ¿Los métodos describen acciones que el negocio reconoce?
- ¿Un experto del dominio entendería el código sin explicación?
- ¿El glosario está actualizado con todos los términos?
- ¿Se evitan sinónimos para el mismo concepto?