← dev-notes
GOF · CREACIONALES

Patrones
Creacionales

Cada patrón en su propia sección: idea, cuándo se nota el problema y refactor en Java

01Factory Method
DEFINICIÓN

Define una operación de creación (factory method) y delega en subclases la decisión de qué clase concreta instanciar. El cliente depende del producto abstracto, no del new concreto.

Cuándo se rompe. Cada vez que añades un tipo exportable (CSV, XLSX, Parquet…), el caso de uso o el menú de UI acumula switch o if sobre strings y conoce nombres de clases concretas.

Sin patrón — el servicio elige la implementación con condicionales

ACOPLAMIENTO A CLASES CONCRETAS
public final class ReportExportService {

    public byte[] export(String format, ReportData data) {
        if ("csv".equalsIgnoreCase(format)) {
            return new CsvReportExporter().export(data);
        }
        if ("pdf".equalsIgnoreCase(format)) {
            return new PdfReportExporter().export(data);
        }
        throw new IllegalArgumentException("Formato no soportado: " + format);
    }
}

Con Factory Method — la familia de creadores encapsula el new

CREADOR ABSTRACTO + SUBCLASES
public interface ReportExporter {
    byte[] export(ReportData data);
}

public abstract class ReportExporterFactory {
    public final byte[] exportReport(ReportData data) {
        ReportExporter exporter = createExporter();
        return exporter.export(data);
    }
    protected abstract ReportExporter createExporter();
}

public final class CsvExporterFactory extends ReportExporterFactory {
    @Override protected ReportExporter createExporter() {
        return new CsvReportExporter();
    }
}

public final class PdfExporterFactory extends ReportExporterFactory {
    @Override protected ReportExporter createExporter() {
        return new PdfReportExporter();
    }
}
02Abstract Factory
DEFINICIÓN

Proporciona una interfaz para crear familias de objetos relacionados (p. ej. almacenamiento + cola + identidad) sin fijar clases concretas, para que un entorno (AWS, GCP, local) sea sustituible de una vez.

Cuándo se rompe. Mezclas en el mismo flujo S3Client, SqsClient y credenciales de AWS con ramas que a veces llaman a emuladores locales: combinaciones inválidas y tests frágiles.

Sin patrón — el caso de uso ensambla piezas de distintos proveedores a mano

FAMILIA INCONSISTENTE / DIFÍCIL DE CAMBIAR
public final class IngestionPipeline {

    public void run(MessagePayload payload) {
        var storage = new AwsS3BlobStore();      // AWS
        var queue = new GcpPubSubQueue();        // GCP mezclado
        var idp = new AwsCognitoIdp();           // otra vez AWS
        byte[] body = storage.read(payload.blobKey());
        queue.publish("processed", body);
        idp.assertUserActive(payload.userId());
    }
}

Con Abstract Factory — una fábrica entrega toda la familia coherente

INTERFAZ DE FAMILIA + IMPLEMENTACIÓN POR ENTORNO
public interface CloudToolkitFactory {
    BlobStore blobStore();
    MessageQueue messageQueue();
    IdentityProvider identityProvider();
}

public final class AwsToolkitFactory implements CloudToolkitFactory {
    public BlobStore blobStore() { return new AwsS3BlobStore(); }
    public MessageQueue messageQueue() { return new AwsSqsQueue(); }
    public IdentityProvider identityProvider() { return new AwsCognitoIdp(); }
}

public final class IngestionPipeline {

    private final CloudToolkitFactory cloud;

    public IngestionPipeline(CloudToolkitFactory cloud) {
        this.cloud = cloud;
    }

    public void run(MessagePayload payload) {
        BlobStore storage = cloud.blobStore();
        MessageQueue queue = cloud.messageQueue();
        IdentityProvider idp = cloud.identityProvider();
        byte[] body = storage.read(payload.blobKey());
        queue.publish("processed", body);
        idp.assertUserActive(payload.userId());
    }
}
03Builder
DEFINICIÓN

Separa la construcción de un objeto complejo de su representación, permitiendo pasos opcionales, validación al final y lectura fluida frente a constructores enormes.

Cuándo se rompe. Objetos con muchos parámetros opcionales (null, false, 0 como sentinela) o constructores de 12 argumentos que nadie recuerda en qué orden van.

Sin patrón — constructor o “mega-setter” ilegible

PARÁMETROS OPCIONALES COMO CAOS
public final class SearchRequest {

    public SearchRequest(String query, int page, int size,
                         String sort, boolean fuzzy, String tenant,
                         Duration timeout, Map<String, String> filters) {
        // ¿qué pasa si page < 0? ¿sort puede ser null?
    }
}

// Uso:
var req = new SearchRequest("invoice", 0, 20, "date", true, "acme",
        Duration.ofSeconds(5), Map.of());

Con Builder — pasos nombrados y validación centralizada

BUILDER FLUIDO
public final class SearchRequest {
    private final String query;
    private final int page;
    private final int size;
    private final String sort;
    private final boolean fuzzy;
    private final String tenant;
    private final Duration timeout;
    private final Map<String, String> filters;

    private SearchRequest(Builder b) {
        if (b.query == null || b.query.isBlank())
            throw new IllegalArgumentException("query requerido");
        if (b.page < 0 || b.size <= 0)
            throw new IllegalArgumentException("paginación inválida");
        this.query = b.query;
        this.page = b.page;
        this.size = b.size;
        this.sort = b.sort != null ? b.sort : "relevance";
        this.fuzzy = b.fuzzy;
        this.tenant = Objects.requireNonNull(b.tenant, "tenant");
        this.timeout = b.timeout != null ? b.timeout : Duration.ofSeconds(3);
        this.filters = Map.copyOf(b.filters);
    }

    public static Builder builder() { return new Builder(); }

    public static final class Builder {
        private String query;
        private int page;
        private int size = 20;
        private String sort;
        private boolean fuzzy;
        private String tenant;
        private Duration timeout;
        private final Map<String, String> filters = new LinkedHashMap<>();

        public Builder query(String q) { this.query = q; return this; }
        public Builder page(int p) { this.page = p; return this; }
        public Builder fuzzy(boolean f) { this.fuzzy = f; return this; }
        public Builder tenant(String t) { this.tenant = t; return this; }
        public Builder filter(String k, String v) { filters.put(k, v); return this; }

        public SearchRequest build() { return new SearchRequest(this); }
    }
}
04Singleton
DEFINICIÓN

Garantiza una única instancia de una clase y un punto de acceso global a ella. En aplicaciones modernas suele sustituirse por inyección de dependencias; el patrón sigue siendo útil para recursos verdaderamente únicos (p. ej. configuración inmutable).

Cuándo se rompe. Estado mutable global, tests que dependen del orden de ejecución, o getInstance() oculto dentro de capas de dominio que deberían ser puros.

Sin patrón — mutable estático accesible desde cualquier sitio

ESTADO GLOBAL Y DIFÍCIL DE RESETEAR EN TESTS
public final class FeatureFlags {

    private static FeatureFlags instance;
    private final Map<String, Boolean> flags = new ConcurrentHashMap<>();

    public static synchronized FeatureFlags getInstance() {
        if (instance == null) instance = new FeatureFlags();
        return instance;
    }

    public void set(String key, boolean on) { flags.put(key, on); }
    public boolean isOn(String key) { return flags.getOrDefault(key, false); }
}

Con Singleton disciplinado — enum o instancia única inyectada

ENUM (THREAD-SAFE) O BEAN ÚNICO EN EL CONTENEDOR
/** Preferido cuando el singleton es realmente inmutable de aplicación */
public enum AppMetadata {
    INSTANCE;

    private final String buildId = readBuildId();

    public String buildId() { return buildId; }

    private static String readBuildId() {
        return System.getenv().getOrDefault("BUILD_ID", "dev");
    }
}

/** Alternativa: una sola instancia creada al arranque y pasada por constructor */
public final class FeatureFlags {
    private final Map<String, Boolean> flags;

    public FeatureFlags(Map<String, Boolean> initial) {
        this.flags = new ConcurrentHashMap<>(initial);
    }

    public boolean isOn(String key) { return flags.getOrDefault(key, false); }
}