← dev-notes
GOF · ESTRUCTURALES

Patrones
Estructurales

Una sección por patrón: definición, olores y refactor en Java

01Adapter
DEFINICIÓN

Convierte la interfaz de una clase en otra que el cliente espera. Actúa como traductor entre tu dominio y una API legada o de terceros sin contaminar el núcleo con sus tipos.

Cuándo se rompe. Servicios de dominio importan paquetes del SDK del proveedor y llaman métodos con nombres y unidades que no encajan con tu lenguaje ubicuo.

Sin patrón — el dominio habla el dialecto del proveedor

ACOPLAMIENTO DIRECTO AL SDK LEGADO
/** Nuestro dominio */
public final class BillingService {
    public void chargeCustomer(Money amount, CustomerId id) {
        // LegacyChargeApi usa centavos long y strings raros
        var api = new com.legacy.LegacyChargeApi();
        api.perform_charge(id.value().replace("-", ""), amount.cents());
    }
}

Con Adapter — el dominio solo ve tu puerto

ADAPTADOR AISLADO
public interface PaymentGateway {
    void charge(CustomerId id, Money amount);
}

public final class LegacyPaymentAdapter implements PaymentGateway {
    private final com.legacy.LegacyChargeApi legacy = new com.legacy.LegacyChargeApi();

    @Override
    public void charge(CustomerId id, Money amount) {
        String legacyId = id.value().replace("-", "");
        legacy.perform_charge(legacyId, amount.cents());
    }
}

public final class BillingService {
    private final PaymentGateway payments;

    public BillingService(PaymentGateway payments) {
        this.payments = payments;
    }

    public void chargeCustomer(Money amount, CustomerId id) {
        payments.charge(id, amount);
    }
}
02Facade
DEFINICIÓN

Ofrece una interfaz unificada y de alto nivel a un subsistema complejo (varias clases, reglas de orden, transacciones). No oculta el subsistema para quien lo necesita detallado; simplifica el caso de uso frecuente.

Cuándo se rompe. Cada controlador HTTP repite la misma secuencia de cinco servicios, mismas validaciones y mismo manejo de errores.

Sin patrón — orquestación duplicada en cada entrada

CONTROLLER CONOCE DEMASIADOS PASOS
public final class CheckoutController {

    public void post(CheckoutRequest req) {
        inventory.reserve(req.sku(), req.qty());
        pricing.applyCoupons(req.userId(), req.couponCodes());
        var payment = payments.authorize(req.cardToken(), req.total());
        shipments.schedule(req.address(), req.sku(), req.qty());
        notifications.sendOrderConfirmation(req.email(), req.orderId());
    }
}

Con Facade — un solo punto de entrada de aplicación

FACHADA DE CASO DE USO
public final class CheckoutFacade {

    private final InventoryService inventory;
    private final PricingService pricing;
    private final PaymentService payments;
    private final ShipmentService shipments;
    private final NotificationService notifications;

    public CheckoutFacade(/* ... */) { /* inyectar */ }

    public CheckoutResult checkout(CheckoutRequest req) {
        inventory.reserve(req.sku(), req.qty());
        pricing.applyCoupons(req.userId(), req.couponCodes());
        var payment = payments.authorize(req.cardToken(), req.total());
        shipments.schedule(req.address(), req.sku(), req.qty());
        notifications.sendOrderConfirmation(req.email(), req.orderId());
        return new CheckoutResult(payment.authorizationId());
    }
}

public final class CheckoutController {
    private final CheckoutFacade checkout;

    public void post(CheckoutRequest req) {
        checkout.checkout(req);
    }
}
03Decorator
DEFINICIÓN

Adjunta responsabilidades adicionales a un objeto de forma dinámica y transparente. Alternativa flexible a subclases por cada combinación (logging + métricas + caché).

Cuándo se rompe. Explosión de subclases CachedLoggedMetricsUserRepository o copiar-pegar el mismo if (cache) en cada método.

Sin patrón — herencia para cada “capa” transversal

COMBINATORIA DE SUBCLASES
public interface UserRepository { Optional<User> findById(long id); }

public class LoggingUserRepository implements UserRepository { /* ... */ }
public class CachedLoggingUserRepository extends LoggingUserRepository { /* ... */ }
// ¿Y si solo quieres caché sin logging? La herencia no combina bien.

Con Decorator — composición en cadena sobre la misma interfaz

DECORADORES APILABLES
public interface UserReader {
    Optional<User> findById(long id);
}

public final class DbUserReader implements UserReader {
    public Optional<User> findById(long id) { /* JDBC */ return Optional.empty(); }
}

public final class LoggingUserReader implements UserReader {
    private final UserReader delegate;
    public LoggingUserReader(UserReader delegate) { this.delegate = delegate; }

    public Optional<User> findById(long id) {
        System.out.println("findById " + id);
        return delegate.findById(id);
    }
}

public final class CachingUserReader implements UserReader {
    private final UserReader delegate;
    private final Map<Long, User> cache = new ConcurrentHashMap<>();

    public CachingUserReader(UserReader delegate) { this.delegate = delegate; }

    public Optional<User> findById(long id) {
        return Optional.ofNullable(cache.computeIfAbsent(id,
            k -> delegate.findById(k).orElse(null)));
    }
}

// Montaje: new CachingUserReader(new LoggingUserReader(new DbUserReader()))
04Proxy
DEFINICIÓN

Proporciona un sustituto que controla el acceso al objeto real: carga perezosa, permisos, contención de coste, reintentos, etc. La interfaz vista por el cliente es la misma que la del sujeto real.

Cuándo se rompe. Cada llamante repite comprobaciones de permiso o lógica de “solo cargar si hace falta” antes de usar el servicio caro.

Sin patrón — control disperso antes de cada uso

VALIDACIÓN Y LAZY EN CADA CLIENTE
public final class ReportController {

    public byte[] monthly(long userId, YearMonth month) {
        if (!access.canReadReports(userId))
            throw new ForbiddenException();
        // generar PDF es caro — algunos callers olvidan el guard
        return heavyReportService.buildMonthlyPdf(userId, month);
    }
}

Con Proxy — un único lugar intercepta el acceso

PROXY DE CONTROL DE ACCESO (Y LAZY SI HACE FALTA)
public interface MonthlyReportService {
    byte[] buildMonthlyPdf(long userId, YearMonth month);
}

public final class HeavyMonthlyReportService implements MonthlyReportService {
    public byte[] buildMonthlyPdf(long userId, YearMonth month) {
        return /* render costoso */;
    }
}

public final class SecuredMonthlyReportProxy implements MonthlyReportService {

    private final AccessControl access;
    private final MonthlyReportService delegate;

    public SecuredMonthlyReportProxy(AccessControl access, MonthlyReportService delegate) {
        this.access = access;
        this.delegate = delegate;
    }

    public byte[] buildMonthlyPdf(long userId, YearMonth month) {
        if (!access.canReadReports(userId))
            throw new ForbiddenException();
        return delegate.buildMonthlyPdf(userId, month);
    }
}
05Composite
DEFINICIÓN

Compone objetos en estructuras de árbol para representar jerarquías parte-todo. Los clientes tratan hojas y compuestos de forma uniforme mediante una interfaz común.

Cuándo se rompe. Listas mezcladas de “carpetas” e “ítems” con instanceof y bucles que duplican lógica en cada nivel.

Sin patrón — ramas con instanceof y duplicación

TRATAMIENTO DESIGUAL DE NODOS
public void renderSidebar(List<Object> nodes) {
    for (Object n : nodes) {
        if (n instanceof MenuSection section) {
            System.out.println("> " + section.title());
            for (MenuItem item : section.items()) {
                System.out.println("  - " + item.label());
            }
        } else if (n instanceof MenuItem item) {
            System.out.println("- " + item.label());
        }
    }
}

Con Composite — un solo contrato recursivo

NODO COMÚN + HOJA Y COMPUESTO
public interface MenuNode {
    void render(String indent);
}

public final class MenuItem implements MenuNode {
    private final String label;
    public MenuItem(String label) { this.label = label; }
    public void render(String indent) {
        System.out.println(indent + "- " + label);
    }
}

public final class MenuSection implements MenuNode {
    private final String title;
    private final List<MenuNode> children = new ArrayList<>();

    public MenuSection(String title) { this.title = title; }
    public void add(MenuNode n) { children.add(n); }

    public void render(String indent) {
        System.out.println(indent + "> " + title);
        for (MenuNode c : children) c.render(indent + "  ");
    }
}