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 Limpia80 XP8 min

Puertos y Adaptadores (Arquitectura Hexagonal)

El interior es el dominio; el exterior es la infraestructura — el exterior depende del interior, los puertos hablan el lenguaje del dominio y los adaptadores traducen entre el dominio y los sistemas externos.

Por qué importa

La Arquitectura Hexagonal (Puertos y Adaptadores), introducida por Alistair Cockburn, organiza un sistema en dos zonas: el interior (entidades de dominio + casos de uso + interfaces de puertos) y el exterior (adaptadores para HTTP, base de datos, correo electrónico, colas). La regla cardinal: el exterior depende del interior — nunca al revés.

Los Puertos son interfaces definidas en el dominio usando lenguaje de dominio. Declaran lo que el dominio necesita, no cómo se proveerá. Hay dos tipos: Puertos conductores (llamados por adaptadores para disparar casos de uso — p.ej., controlador HTTP llamando a un caso de uso) y Puertos conducidos (implementados por adaptadores a petición del dominio — p.ej., una interfaz de repositorio implementada por un adaptador PostgreSQL).

El nombre del puerto es crítico. Un puerto pertenece al dominio y debe hablar el lenguaje del dominio. Un puerto llamado OrderPersistenceGateway con un método persistEntity() ha sido contaminado por vocabulario de infraestructura. El puerto debería llamarse Orders con un método save(). El adaptador traduce; el puerto describe.

✗El problema

Port interface named OrderPersistenceGateway with methods using infrastructure vocabulary — infrastructure concepts leak into the domain language.

Bad

from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class OrderEntity:
    id: str
    total: float
    status: str

# Named "port" but speaks infrastructure language
class OrderPersistenceGateway(ABC):    # ← technical name
    @abstractmethod
    def fetchById(self, record_id: str) -> OrderEntity: ...  # "fetch", "record"

    @abstractmethod
    def persistEntity(self, entity: OrderEntity) -> None: ...  # "persist", "entity"

    @abstractmethod
    def removeRecord(self, record_id: str) -> None: ...        # "record"

# A business analyst reading this sees SQL vocabulary, not business operations.
# The infrastructure is leaking into the domain through naming.
// domain/ports/OrderPersistenceGateway.ts  ← wrong name, wrong language
export interface OrderEntity { id: string; total: number; status: string; }

export interface OrderPersistenceGateway {
  fetchById(recordId: string): Promise;    // "fetch", "record" ← DB language
  persistEntity(entity: OrderEntity): Promise;   // "persist", "entity" ← ORM language
  removeRecord(recordId: string): Promise;        // "record" ← DB language
}

// Infrastructure vocabulary leaked into the domain.
// This port cannot be read without thinking about databases.

✓La solución

Port Orders with methods find, save, remove — pure domain language. PostgresOrders and InMemoryOrders both implement the domain port.

Good

from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class Order:
    id: str
    total: float
    status: str

# domain/ports/orders.py  ← pure domain language
class Orders(ABC):                              # domain name: "Orders"
    @abstractmethod
    def find(self, order_id: str) -> Order: ... # business verb: "find"

    @abstractmethod
    def save(self, order: Order) -> None: ...   # business verb: "save"

    @abstractmethod
    def remove(self, order_id: str) -> None: ... # business verb: "remove"

# adapters/postgres_orders.py  ← adapter translates domain to SQL
import psycopg2

class PostgresOrders(Orders):
    def __init__(self, dsn: str): self._conn = psycopg2.connect(dsn)

    def find(self, order_id: str) -> Order:
        cur = self._conn.cursor()
        cur.execute("SELECT id, total, status FROM orders WHERE id = %s", (order_id,))
        row = cur.fetchone()
        return Order(id=row[0], total=row[1], status=row[2])

    def save(self, order: Order) -> None:
        cur = self._conn.cursor()
        cur.execute(
            "INSERT INTO orders (id, total, status) VALUES (%s, %s, %s) "
            "ON CONFLICT (id) DO UPDATE SET total=%s, status=%s",
            (order.id, order.total, order.status, order.total, order.status)
        )
        self._conn.commit()

    def remove(self, order_id: str) -> None:
        self._conn.cursor().execute("DELETE FROM orders WHERE id = %s", (order_id,))
        self._conn.commit()

# adapters/in_memory_orders.py  ← test adapter
class InMemoryOrders(Orders):
    def __init__(self): self._store: dict[str, Order] = {}
    def find(self, order_id: str) -> Order:  return self._store[order_id]
    def save(self, order: Order) -> None:    self._store[order.id] = order
    def remove(self, order_id: str) -> None: del self._store[order_id]
// domain/ports/Orders.ts  ← pure domain language
export interface Order { id: string; total: number; status: string; }

export interface Orders {                    // domain name: "Orders"
  find(orderId: string): Promise;    // business verb: "find"
  save(order: Order): Promise;        // business verb: "save"
  remove(orderId: string): Promise;   // business verb: "remove"
}

// adapters/PostgresOrders.ts  ← adapter translates domain to SQL
import { Pool } from "pg";
import { Orders, Order } from "../domain/ports/Orders";

export class PostgresOrders implements Orders {
  constructor(private pool: Pool) {}

  async find(orderId: string): Promise {
    const result = await this.pool.query(
      "SELECT id, total, status FROM orders WHERE id = $1", [orderId]
    );
    return result.rows[0] as Order;
  }

  async save(order: Order): Promise {
    await this.pool.query(
      "INSERT INTO orders (id, total, status) VALUES ($1, $2, $3) " +
      "ON CONFLICT (id) DO UPDATE SET total=$2, status=$3",
      [order.id, order.total, order.status]
    );
  }

  async remove(orderId: string): Promise {
    await this.pool.query("DELETE FROM orders WHERE id = $1", [orderId]);
  }
}

// adapters/InMemoryOrders.ts  ← test adapter
import { Orders, Order } from "../domain/ports/Orders";

export class InMemoryOrders implements Orders {
  private store = new Map();
  async find(orderId: string): Promise  { return this.store.get(orderId)!; }
  async save(order: Order):    Promise   { this.store.set(order.id, order); }
  async remove(orderId: string): Promise { this.store.delete(orderId); }
}

💡Conclusión clave

Los puertos pertenecen al dominio y hablan su lenguaje. Si un método de puerto se llama fetchRecord en lugar de findOrder, la infraestructura está filtrándose en tu dominio. El trabajo del adaptador es traducir; el trabajo del puerto es describir lo que el negocio necesita con las propias palabras del negocio.

🔧 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: Los puertos pertenecen al dominio y hablan su lenguaje. Si un método de puerto se llama 'fetchRecord' en lugar de 'findOrder', la infraestructura está filtrándose en tu dominio.

✗ Tu versión

Puertos y Adaptadores (Arquitectura Hexagonal) — CleanKata — CleanKata