Una sección por patrón: definición, olores y refactor en Java
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
/** 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
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);
}
}
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
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
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);
}
}
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
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
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()))
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
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
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);
}
}
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
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
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 + " ");
}
}