Skip to main content

Inicia sesión en CleanKata

Sigue tu progreso, gana XP y desbloquea todas las lecciones.

Al iniciar sesión aceptas nuestros Términos de uso y Política de privacidad.

Arquitectura Limpia70 XP7 min

El Patrón Humble Object

Separa el comportamiento difícil de probar del fácil de probar — el Presentador prepara toda la lógica en un ViewModel testeable; la Vista es un humble object que solo muestra lo que recibe.

Por qué importa

Algunos objetos son difíciles de probar porque están fuertemente acoplados al entorno: una Vista de GUI requiere un framework de UI en ejecución, un disparador de base de datos requiere una base de datos activa. El patrón Humble Object resuelve esto dividiendo dichos objetos en dos partes: la parte humilde que es difícil de probar (la Vista, el disparador), y la parte testeable que es fácil de probar (el Presentador, la lógica).

La idea: mover TODA la lógica a la parte testeable. El Presentador calcula cada cadena de texto, cada booleano, cada orden de clasificación, cada fecha formateada y cada etiqueta condicional antes de que la Vista se ejecute. La Vista recibe un ViewModel que contiene solo cadenas y booleanos preformateados — los renderiza directamente, sin ninguna lógica condicional propia.

El resultado: el Presentador puede probarse exhaustivamente como una función pura — sin navegador, sin jsdom, sin motor de plantillas. La Vista es tan simple que no puede tener un bug. Si un umbral de envío cambia de $100 a $150, actualizas el Presentador y la prueba — nunca la plantilla.

✗El problema

Plantillas y componentes con lógica condicional — umbrales de envío, formato de fechas, colores de estado — todos inprobables sin renderizar la UI completa.

Bad

# order_detail.html (Jinja2 template with logic):
#
# {% if order.total > 100 %}
#   <span class="badge green">Free Shipping</span>
# {% else %}
#   <span class="badge red">+ $9.99 Shipping</span>
# {% endif %}
#
# <span class="status {{ 'green' if order.status == 'paid' else 'red' }}">
#   {{ order.status | upper }}
# </span>
# <p>Ordered on: {{ order.created_at.strftime('%d/%m/%Y') }}</p>
# <p>Total: ${{ "%.2f" % order.total }}</p>
#
# All logic — shipping threshold, status color, date/money format —
# is trapped in the template. You cannot unit-test it.
# A bug in the shipping threshold goes undetected until production.
// React component with business logic in JSX:
export function OrderDetail({ order }: { order: Order }) {
  return (
    <div>
      <span className={`badge ${order.total > 100 ? "green" : "red"}`}>
        {order.total > 100 ? "Free Shipping" : "+ $9.99 Shipping"}
      </span>
      <span className={`status ${order.status === "paid" ? "green" : "red"}`}>
        {order.status.toUpperCase()}
      </span>
      <p>{new Date(order.createdAt).toLocaleDateString("en-GB")}</p>
    </div>
  );
}
// Testing requires React Testing Library, jsdom, and rendering.
// Logic hidden in JSX is easy to miss and hard to refactor.

✓La solución

The Presenter computes a ViewModel with pre-formatted strings and booleans. The template or component is humble — it only renders what it receives, with zero conditional logic.

Good

from dataclasses import dataclass
from decimal import Decimal

@dataclass
class OrderViewModel:
    total_display:  str   # "$127.50"
    shipping_label: str   # "Free Shipping" or "+ $9.99 Shipping"
    date_display:   str   # "15/03/2024"
    status_label:   str   # "PAID"
    status_color:   str   # "green" or "red"

class OrderPresenter:
    THRESHOLD = Decimal("100.00")

    def present(self, order) -> OrderViewModel:
        free = order.total >= self.THRESHOLD
        return OrderViewModel(
            total_display  = f"${order.total:,.2f}",
            shipping_label = "Free Shipping" if free else "+ $9.99 Shipping",
            date_display   = order.created_at.strftime("%d/%m/%Y"),
            status_label   = order.status.upper(),
            status_color   = "green" if order.status == "paid" else "red",
        )

# Template (humble — zero logic):
# <span class="{{ vm.status_color }}">{{ vm.shipping_label }}</span>
# <p>{{ vm.date_display }}</p>

# Unit test — no template rendering:
# vm = OrderPresenter().present(order)
# assert vm.shipping_label == "Free Shipping"
export interface OrderViewModel {
  totalDisplay:   string;   // "$127.50"
  shippingLabel:  string;   // "Free Shipping" | "+ $9.99 Shipping"
  dateDisplay:    string;   // "15/03/2024"
  statusLabel:    string;   // "PAID"
  statusColor:    string;   // "green" | "red"
}

// Presenter — pure function, fully unit-testable with plain Jest:
export function presentOrder(order: Order): OrderViewModel {
  const free = order.total > 100;
  return {
    totalDisplay:  \`$\${order.total.toFixed(2)}\`,
    shippingLabel: free ? "Free Shipping" : "+ $9.99 Shipping",
    dateDisplay:   new Date(order.createdAt).toLocaleDateString("en-GB"),
    statusLabel:   order.status.toUpperCase(),
    statusColor:   order.status === "paid" ? "green" : "red",
  };
}

// Component (humble — just renders the ViewModel, no logic):
export function OrderDetail({ order }: { order: Order }) {
  const vm = presentOrder(order);
  return (
    <div>
      <span className={vm.statusColor}>{vm.shippingLabel}</span>
      <p>{vm.dateDisplay}</p>
      <p>{vm.totalDisplay}</p>
    </div>
  );
}
// Unit test — no React, no jsdom:
// expect(presentOrder(order).shippingLabel).toBe("Free Shipping");

💡Conclusión clave

Si tu plantilla tiene un if, tiene lógica que no se está probando. Mueve toda la lógica condicional, el formato y los valores calculados al Presentador. Haz la Vista tan simple que no pueda tener un bug — luego prueba el Presentador exhaustivamente como una función pura, sin ninguna infraestructura de UI.

🔧 Algunos ejercicios pueden tener errores. Si algo parece incorrecto, usa el botón Feedback (abajo a la derecha) para reportarlo — nos ayuda a corregirlo rápido.

Pista: Si tu plantilla tiene un if, tiene lógica que no se está probando. Muévela al Presentador.

✗ Tu versión