tactical-collab · temario
4.5tema 5 de 5

Specifications

Reglas de negocio como objetos componibles: and, or, not aplicados a criterios del dominio.

Una Specification encapsula una regla de negocio como un objeto independiente con un método `is_satisfied_by(candidate)`. Su propósito principal es hacer las reglas de negocio explícitas, nombradas y reutilizables en lugar de dispersarlas como condicionales anónimos en aggregates, servicios o consultas.

El poder de las Specifications está en su composabilidad. Tres operaciones básicas — `and_`, `or_`, `not_` — permiten combinar specifications simples para construir reglas complejas sin duplicar código. Una `ActiveAndVerifiedCustomerSpecification` puede construirse combinando `ActiveCustomerSpecification` y `VerifiedCustomerSpecification` con un `AndSpecification`.

Las Specifications tienen tres usos principales: validación (verificar si un objeto cumple una regla antes de una operación), selección en memoria (filtrar colecciones de aggregates) y construcción de criterios de consulta (traducir la regla a SQL o filtros de repositorio). La misma clase puede cumplir los tres roles.

En Python, la base se implementa con una clase abstracta genérica que define `is_satisfied_by(candidate: T) -> bool`. Las implementaciones de `AndSpecification`, `OrSpecification` y `NotSpecification` usan composición y hacen innecesario heredar para combinar.

Las Specifications viven en la capa de dominio (`domain/specifications/`). No tienen dependencias de infraestructura. Si una Specification necesita datos externos para evaluarse (por ejemplo, comprobar si un cliente tiene deudas activas), esos datos deben llegar como parámetros o a través de un puerto inyectado, nunca accediendo directamente a la BD.

No todas las reglas de negocio merecen una Specification. Para reglas simples y de un solo uso, un método booleano en el aggregate o Value Object es suficiente y más directo. Las Specifications brillan cuando la regla se reutiliza en múltiples contextos (validación + selección + consulta) o cuando la composición de reglas es necesaria.

structure.txt
# domain/specifications/base_specification.py
# Contrato base: toda Specification implementa is_satisfied_by.
# La composición (and/or/not) es gratuita al heredar de esta base.
from abc import ABC, abstractmethod
from typing import TypeVar, Generic

T = TypeVar('T')

class Specification(ABC, Generic[T]):
    @abstractmethod
    def is_satisfied_by(self, candidate: T) -> bool:
        """Evalúa si el candidato cumple la regla. Sin efectos secundarios."""
        ...

    def and_(self, other: 'Specification[T]') -> 'Specification[T]':
        return _AndSpecification(self, other)

    def or_(self, other: 'Specification[T]') -> 'Specification[T]':
        return _OrSpecification(self, other)

    def not_(self) -> 'Specification[T]':
        return _NotSpecification(self)


class _AndSpecification(Specification[T]):
    def __init__(self, left: Specification[T], right: Specification[T]) -> None:
        self._left = left
        self._right = right

    def is_satisfied_by(self, candidate: T) -> bool:
        return self._left.is_satisfied_by(candidate) and self._right.is_satisfied_by(candidate)


class _OrSpecification(Specification[T]):
    def __init__(self, left: Specification[T], right: Specification[T]) -> None:
        self._left = left
        self._right = right

    def is_satisfied_by(self, candidate: T) -> bool:
        return self._left.is_satisfied_by(candidate) or self._right.is_satisfied_by(candidate)


class _NotSpecification(Specification[T]):
    def __init__(self, wrapped: Specification[T]) -> None:
        self._wrapped = wrapped

    def is_satisfied_by(self, candidate: T) -> bool:
        return not self._wrapped.is_satisfied_by(candidate)


# domain/specifications/<rule>_specification.py
# Regla concreta del dominio con nombre en el lenguaje ubicuo.
from .<base_specification> import Specification
from ..<model>.<aggregate> import <Aggregate>

class <BusinessRule>Specification(Specification[<Aggregate>]):
    """<Describe la regla en lenguaje de negocio.>"""

    def is_satisfied_by(self, candidate: <Aggregate>) -> bool:
        # Lógica pura de dominio: sin I/O, sin efectos secundarios
        return candidate.<attribute> <condition>


# ── Composición ──────────────────────────────────────────────────────────────
# application o domain service que construye reglas compuestas:
eligible_spec = (
    <BusinessRule>Specification()
    .and_(<AnotherRule>Specification())
    .and_(<ThirdRule>Specification().not_())
)

# Uso para validación:
if not eligible_spec.is_satisfied_by(candidate):
    raise <DomainError>('<reason>')

# Uso para selección en memoria:
eligible_items = [item for item in items if eligible_spec.is_satisfied_by(item)]
project/
<project>
contexts
<bounded_context>
domain
specifications
model
<bounded_context>

Debugging lab

Detecta y corrige el error o la violación de diseño.

0/5 tests passing0%
  1. 4.5.5.1

    # domain/specifications/premium_customer_specification.py class PremiumCustomerSpecification(Specification[Customer]): def is_satisfied_by(self, candidate: Customer) -> bool: self._db.query(...) # ← error: I/O en Specification raise ValueError('not premium') # ← error: lanza excepción en is_satisfied_by

  2. 4.5.5.2

    # domain/model/order.py (fragmento) class Order: def can_be_shipped(self) -> bool: return ( self.status == OrderStatus.PAID and self.items and not self.is_flagged_for_review and self.shipping_address is not None and self.weight_kg <= MAX_WEIGHT ) # ← regla compleja y no reutilizable dispersa en el aggregate

  3. 4.5.5.3

    # domain/specifications/active_account_specification.py class ActiveAccountSpecification(Specification[Account]): def is_satisfied_by(self, candidate: Account) -> bool: return candidate.is_active # Uso en el handler: if not ActiveAccountSpecification().is_satisfied_by(account): pass # ← no hace nada cuando falla; no lanza error de dominio

  4. 4.5.5.4

    # Intento de composición sin usar and_/or_/not_: class EligibleForLoanSpecification(Specification[Customer]): def is_satisfied_by(self, candidate: Customer) -> bool: # ← reimplementa la lógica de ActiveCustomerSpec y GoodCreditSpec return candidate.is_active and candidate.credit_score >= 650

  5. 4.5.5.5

    # domain/specifications/recent_order_specification.py from datetime import datetime class RecentOrderSpecification(Specification[Order]): def is_satisfied_by(self, candidate: Order) -> bool: # ← datetime.now() sin timezone: comportamiento no determinista return (datetime.now() - candidate.created_at).days <= 30