Dismitificando SOLID: La Magia del Principio Abierto/Cerrado (OCP)

Dismitificando SOLID: La Magia del Principio Abierto/Cerrado (OCP)
Photo by Kenny Eliason / Unsplash

¡Hola nuevamente!

En la entrada anterior abarcamos el Principio de Responsabilidad Única (SRP) y cómo nos ayuda a mantener nuestras clases enfocadas a una tarea. El día de hoy, continuaremos con el siguiente paso en SOLID y exploraremos la "O", donde miraremos el concepto de Principio de Abierto/Cerrado (Open/Closed Principle - OCP).

Estoy seguro de que alguna vez nos encontramos con la necesidad de cambiar código existente, pero con el riesgo latente de romper algo que ya funciona bien, de ahí la mítica frase "si funciona no lo toques". Para esta enfermedad, tenemos la medicina y se llama OCP, nos ofrece una vía para extender funcionalidades de nuestro código sin riesgo de dañar lo existente.

¿Qué es el Principio de Abierto/Cerrado (OCP)?

Originalmente formulado por Bertrand Meyer, y popularizado por Robert C. Martin en el contexto de SOLID, el OCP establece que:

"Las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para la extensión, pero cerradas para la modificación"

Exploremos esto:

  • Abierto para la extensión: Significa que el comportamiento de la entidad puede ser extendido, es decir, si es necesario añadir nuevas funcionalidades debemos ser capaces de hacer que la entidad se comporte de nuevas maneras.
  • Cerrado para modificación: Aquí establecemos que el código existente no se modifica una vez que ha sido aprobado y está en funcionamiento. Si es necesario agregar algo, no se debería modificar lo que ya existe, puesto que otras partes dependen de esa pieza de código.

¿Es para tanto? Beneficios de OCP

El aplicar bien el OCP no solo es un "ejercicio académico"; trae ventajas muy tangibles:

  1. Flexibilidad y Mantenibilidad: Podemos agregar nuevas funcionalidades o comportamientos sin miedo a introducir bugs en el código existente y ya probado.
  2. Reducción de Riesgos: Al no modificar código estable, reducimos las probabilidades de efectos secundarios inesperados.
  3. Reusabilidad Mejorada: Las abstracciones bien definidas que surgen del OCP tienden a ser más reutilizables.
  4. Facilita el Trabajo en Equipo: Diferentes desarrolladores pueden trabajar en nuevas extensiones de forma independiente sin interferir con el núcleo estable del sistema.
  5. Código Más Robusto: El sistema se vuelve más resistente a los cambios a lo largo del tiempo.

OCP en Acción: Ejemplo práctico

Es más fácil dar un ejemplo con código, así que veámoslo.

Imaginemos que tenemos una tienda en línea, necesitamos calcular el precio final de una orden luego de que aplicamos los descuentos necesarios.

Anti-Ejemplo:

Al principio, podemos tener una clase que maneja los cálculos de descuento con una serie de if-else if:

class CalculadoraPreciosAntiOcp {
    public double calcularPrecioFinal(double precioBase, String tipoDescuento, double valorParametroDescuento) {
        double precioConDescuento = precioBase;

        if ("PORCENTAJE".equalsIgnoreCase(tipoDescuento)) {
            // valorParametroDescuento es el porcentaje, ej. 10 para 10%
            precioConDescuento -= precioBase * (valorParametroDescuento / 100.0);
        } else if ("MONTO_FIJO".equalsIgnoreCase(tipoDescuento)) {
            // valorParametroDescuento es el monto fijo a descontar
            precioConDescuento -= valorParametroDescuento;
        }
        // ¿Qué pasa si mañana necesitamos un descuento "COMPRA_GRANDE_X_LLEVA_Y"?
        // ¿O un descuento "ENVIO_GRATIS_SI_SUPERA_MONTO"?
        // ¡Tendríamos que AÑADIR MÁS 'else if' AQUÍ!
        // Esto significa MODIFICAR esta clase ya existente y probada.

        return precioConDescuento > 0 ? precioConDescuento : 0; // Evita precios negativos
    }
}


// --- Para demostrar el uso ---


/*
public class MainAntiOcp {
    public static void main(String[] args) {
        System.out.println("--- Ejecutando con Violación de OCP (Java) ---");
        CalculadoraPreciosAntiOcp calculadora = new CalculadoraPreciosAntiOcp();

        double precio1 = calculadora.calcularPrecioFinal(100.0, "PORCENTAJE", 10.0); // 10% de descuento
        System.out.println("Precio con 10% de descuento: " + precio1); // Esperado: 90.0

        double precio2 = calculadora.calcularPrecioFinal(100.0, "MONTO_FIJO", 15.0); // 15 de descuento
        System.out.println("Precio con 15 de descuento fijo: " + precio2); // Esperado: 85.0

        // Si añadimos un nuevo tipo de descuento "DESCUENTO_VIP_FIJO",
        // tendríamos que modificar la clase CalculadoraPreciosAntiOcp.
        System.out.println("------------------------------------------");
    }
}
*/

En este código encontramos un problema bastante claro: cada vez que añadimos un tipo de descuento, tenemos que modificar código existente. Esto viola totalmente el OCP, ya que modificaríamos algo previamente aprobado.

Solución: Refactorizando

Vamos a usar interfaces y algo clave, el polimorfismo, para poder hacer nuestra calculadora compatible con OCP.

// --- Clases con OCP Aplicado en Java ---

// 1. Definimos una interfaz para la estrategia de descuento
interface EstrategiaDescuento {
    double aplicarDescuento(double precioBase);
}

// 2. Implementaciones concretas para cada tipo de descuento
class DescuentoPorcentaje implements EstrategiaDescuento {
    private double porcentaje;

    public DescuentoPorcentaje(double porcentaje) {
        this.porcentaje = porcentaje;
    }

    @Override
    public double aplicarDescuento(double precioBase) {
        return precioBase - (precioBase * (this.porcentaje / 100.0));
    }
}

class DescuentoMontoFijo implements EstrategiaDescuento {
    private double monto;

    public DescuentoMontoFijo(double monto) {
        this.monto = monto;
    }

    @Override
    public double aplicarDescuento(double precioBase) {
        return precioBase - this.monto;
    }
}

// ¡NUEVO TIPO DE DESCUENTO!
class DescuentoPorVolumen implements EstrategiaDescuento {
    private double umbralCantidad; // Ej: si compra más de 5 unidades
    private double descuentoFijoPorUnidadAdicional; // Ej: $1 menos por cada unidad sobre el umbral

    public DescuentoPorVolumen(double umbralCantidad, double descuentoFijoPorUnidadAdicional) {
        this.umbralCantidad = umbralCantidad; // Supongamos que el precioBase ya considera la cantidad
                                              // o que este descuento se aplica al total.
                                              // Para simplificar, asumimos que precioBase es el total.
        this.descuentoFijoPorUnidadAdicional = descuentoFijoPorUnidadAdicional;
    }

    @Override
    public double aplicarDescuento(double precioBase) {
        // Esta lógica es un ejemplo, podría ser más compleja
        if (precioBase > (umbralCantidad * 10)) { // Ejemplo de umbral basado en precio total
             // Aplicamos un descuento basado en el descuentoFijo.
             // Simplificando, un descuento fijo si supera un umbral de precio total.
            return precioBase - descuentoFijoPorUnidadAdicional;
        }
        return precioBase;
    }
}


// 3. La calculadora ahora usa la interfaz (está CERRADA para modificación, ABIERTA para extensión)
class CalculadoraPreciosOcp {
    public double calcularPrecioFinal(double precioBase, EstrategiaDescuento estrategia) {
        if (estrategia == null) { // Caso sin descuento o estrategia no aplicable
            return precioBase;
        }
        double precioConDescuento = estrategia.aplicarDescuento(precioBase);
        return precioConDescuento > 0 ? precioConDescuento : 0; // Evita precios negativos
    }
}

// --- Para demostrar el uso ---


/*
public class MainOcp {
    public static void main(String[] args) {
        // Demostración con OCP Aplicado
        System.out.println("--- Ejecutando con OCP Aplicado (Java) ---");
        CalculadoraPreciosOcp calculadoraOcp = new CalculadoraPreciosOcp();

        EstrategiaDescuento descuentoPorcentaje = new DescuentoPorcentaje(10.0); // 10%
        double precioOcp1 = calculadoraOcp.calcularPrecioFinal(100.0, descuentoPorcentaje);
        System.out.println("Precio OCP (10%): " + precioOcp1); // Esperado: 90.0

        EstrategiaDescuento descuentoMontoFijo = new DescuentoMontoFijo(15.0); // 15 de descuento
        double precioOcp2 = calculadoraOcp.calcularPrecioFinal(100.0, descuentoMontoFijo);
        System.out.println("Precio OCP (Fijo 15): " + precioOcp2); // Esperado: 85.0

        // ¡Añadiendo un NUEVO tipo de descuento SIN MODIFICAR CalculadoraPreciosOcp!
        // Solo hemos creado la nueva clase DescuentoPorVolumen.
        EstrategiaDescuento descuentoVolumen = new DescuentoPorVolumen(50.0, 20.0); // Umbral de precio 50, descuento de 20
        double precioOcp3 = calculadoraOcp.calcularPrecioFinal(200.0, descuentoVolumen);
        System.out.println("Precio OCP (Volumen, aplica descuento de 20): " + precioOcp3); // Esperado: 180.0
        double precioOcp4 = calculadoraOcp.calcularPrecioFinal(40.0, descuentoVolumen);
        System.out.println("Precio OCP (Volumen, no aplica descuento): " + precioOcp4); // Esperado: 40.0

        System.out.println("------------------------------------------");
    }
}
*/

¡La magia se ha manifestado!

Ahora, si necesitamos un nuevo tipo de descuento, simplemente creamos una nueva implementación de la interfaz. Las clases anteriores no necesitan ser modificadas en nada, esto nos permite ir con seguridad de que el código anterior no dejará de funcionar.

Técnicas comunes

El ejemplo anterior es una de las maneras de aplicar OCP, pero existen otras como:

  • Herencia y clases abstractas el cual también es conocido como patrón Template Method, donde buscamos definir una clase base que servirá como esqueleto y quien se encargará de dar lógica serán sus subclases.
  • Patrón decorador, muy usado en lenguajes como python, donde podemos agregar funcionalidades a objetos de forma dinámica envolviéndolos.
  • Inyección de Dependencias donde nos permitimos cambiar el comportamiento inyectándolo desde una dependencia.

Equilibrio y Pragmatismo

Si bien el OCP es muy poderoso, no podemos decir que debes abstraer cada cosa, cada detalle de tu código desde el primer momento. Aplicar OCP nos agrega niveles algo más de complejidad en ciertos casos, aquí la clave es ser pragmático anticipando los puntos donde el app puede variar y por favor, no te adelantes, si una parte del código es muy estable es poco probable que necesite ser cambiada o que se extienda, abstraer algo antes de tiempo se puede considerar sobreingeniería.

A menudo, necesitaremos empezar con soluciones simples y luego refactorizar hacia OCP cuando necesitemos que una parte del código sea modificada repetidamente para agregar funcionalidades similares.

Conclusion

Este principio es fundamental, nos permite crear software que puede evolucionar con gracia. Al diseñar la estructura del código para que estén abiertas a su extensión pero cerradas a la modificación, construimos sistemas más robustos pero flexibles, y fáciles de mantener.

Hasta aquí la entrada de hoy, en nuestra próxima entrada continuaremos explorando SOLID, donde veremos el concepto de la "L".