← Volver al listado de tecnologías

Capítulo 1: Lenguaje Ubicuo

Por: Artiko
dddlenguaje-ubicuopythonfundamentos

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:

“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:

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 NegocioEn CódigoDescripciónReglas
PedidoPedidoSolicitud de compraDebe tener al menos 1 línea
Confirmar pedidopedido.confirmar()Validar y aceptarSolo si está pagado
Cliente activocliente.esta_activo()Cuenta habilitadaSin bloqueos ni deudas
Línea de pedidoLineaPedidoItem individualCantidad > 0
Preparar envíopedido.iniciar_preparacion()Comenzar pickingSolo 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