6. Principio Abierto/Cerrado (Open/Closed Principle - OCP)

El Principio Abierto/Cerrado (OCP, Open/Closed Principle) afirma que el código debe estar abierto a la extensión pero cerrado a la modificación. Fue formulado por Bertrand Meyer y se convirtió en una piedra angular del diseño orientado a objetos moderno.

Aplicar OCP significa que nuevas funcionalidades se agregan mediante componentes adicionales que colaboran con los existentes, evitando alterar clases ya probadas. Esto disminuye el riesgo de introducir errores y facilita la evolución de sistemas complejos.

6.1 Motivaciones del OCP

Los requerimientos cambian constantemente: surgen nuevas reglas, se agregan integraciones y se modifican procesos internos. Si cada cambio obliga a editar clases que ya funcionan, el proyecto se vuelve frágil. OCP propone encapsular variaciones en extensiones controladas.

6.2 Relación entre OCP y otros principios SOLID

OCP se apoya en DIP e interfaces bien definidas. Para extender sin modificar, el código debe depender de abstracciones estables. A su vez, SRP facilita la extensión porque cada clase cumple un rol específico.

6.3 Señales que indican falta de OCP

  • Condicionales multiplicados: if o switch que crecen con cada nueva variante.
  • Clases que cambian constantemente: los archivos más modificados en el historial de commits suelen violar OCP.
  • Dependencias rígidas: el código depende de clases concretas en lugar de interfaces.
  • Duplicación de lógica: se copian bloques parecidos para “no tocar” la clase original, generando inconsistencias.

6.4 Ejemplo de violación a OCP en Java

Analicemos un servicio que calcula comisiones según el tipo de vendedor. Cada vez que la compañía agrega una categoría nueva, hay que abrir la clase y modificarla.

class CalculadoraComisiones {
    double calcular(Vendedor vendedor) {
        if (vendedor.tipo() == TipoVendedor.JUNIOR) {
            return vendedor.ventas() * 0.02;
        }
        if (vendedor.tipo() == TipoVendedor.SENIOR) {
            return vendedor.ventas() * 0.035;
        }
        if (vendedor.tipo() == TipoVendedor.PARTNER) {
            return vendedor.ventas() * 0.05;
        }
        throw new IllegalArgumentException("Tipo de vendedor no soportado");
    }
}

El código es difícil de mantener: agregar un nuevo tipo implica editar la clase, compilar nuevamente y revalidar todo el flujo.

6.5 Refactorización aplicando OCP

Para cumplir OCP, se define una abstracción para el cálculo de comisiones y se delega la lógica en implementaciones intercambiables. El servicio principal confía en la abstracción y se mantiene cerrado a cambios.

interface PoliticaComision {
    boolean aplicaA(Vendedor vendedor);
    double calcular(Vendedor vendedor);
}

class CalculadoraComisiones {
    private final List<PoliticaComision> politicas;

    CalculadoraComisiones(List<PoliticaComision> politicas) {
        this.politicas = politicas;
    }

    double calcular(Vendedor vendedor) {
        return politicas.stream()
                .filter(politica -> politica.aplicaA(vendedor))
                .findFirst()
                .map(politica -> politica.calcular(vendedor))
                .orElseThrow(() -> new IllegalArgumentException("Tipo no soportado"));
    }
}

Ahora los nuevos tipos se agregan implementando PoliticaComision sin modificar CalculadoraComisiones.

6.6 Implementaciones concretas

class ComisionJunior implements PoliticaComision {
    public boolean aplicaA(Vendedor vendedor) {
        return vendedor.tipo() == TipoVendedor.JUNIOR;
    }

    public double calcular(Vendedor vendedor) {
        return vendedor.ventas() * 0.02;
    }
}

class ComisionSenior implements PoliticaComision {
    public boolean aplicaA(Vendedor vendedor) {
        return vendedor.tipo() == TipoVendedor.SENIOR;
    }

    public double calcular(Vendedor vendedor) {
        return vendedor.ventas() * 0.035;
    }
}

Al registrar una nueva política en la lista (por ejemplo, para vendedores PARTNER), la calculadora queda extendida sin cambiar su código.

6.7 Patrones relacionados con OCP

  • Estrategia: encapsula algoritmos intercambiables, como en el ejemplo anterior.
  • Decorador: añade responsabilidades dinámicamente sin modificar clases existentes.
  • Fábrica: crea objetos sin exponer la lógica de instanciación, permitiendo añadir variantes.
  • Plantilla: define el esqueleto de un algoritmo y delega pasos variables en subclases.

6.8 Beneficios de adoptar OCP

  • Menor riesgo: se evita tocar código en producción, reduciendo defectos.
  • Cohesión mejorada: cada componente enfocado en una variación concreta.
  • Escalabilidad: se agregan nuevas reglas sin afectar a las existentes.
  • Pruebas más simples: las implementaciones se testean de forma aislada y se simulan con facilidad.

6.9 Recomendaciones prácticas

  • Detectar puntos de variación: identificar qué partes del sistema cambian con más frecuencia.
  • Aplicar refactorizaciones incrementales: no intentar un diseño perfecto desde el inicio; extender y ajustar progresivamente.
  • Definir contratos claros: las interfaces deben capturar el comportamiento esencial de cada variación.
  • Revisar con el equipo: validar la estrategia de extensión en sesiones de diseño o revisiones de código.

El Principio Abierto/Cerrado permite construir bases de código evolutivas. En los próximos capítulos exploraremos cómo se complementa con el Principio de Sustitución de Liskov para mantener jerarquías consistentes.