DEV Community

Victor Manuel Pinzon
Victor Manuel Pinzon

Posted on • Updated on

SOLID: Principio de Abierto/Cerrado

Este articulo es una continuación a la publicación de principios SOLID.

El segundo principio SOLID es probablemente el más importante de todos, pero el menos utilizado. La poca inclusión de este principio se ha debido a su débil y confusa definición.

El principio de abierto/cerrado fue acuñado por Bertrand Meyer en su libro Object Oriented Software Construction, quien lo definió de la siguiente manera:

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

La definición original por Meyer es ambigua y poco aplicable en la practica, lo que ocasionó que muchos desarrolladores de la época ignoraran la aplicación de dicho principio. Años despues, Bob Martin amplió la definición de la siguiente manera:

El comportamiento de un sistema debería de ser capaz de ser extendido sin necesidad de modificar dicho sistema.

La explicación dada por Bob Martin es más clara y es compatible con la arquitectura de Plugin, la cual estaba en su apogeo en esa época. Esta arquitectura indica que un sistema de software debería de ser construido con la capacidad de agregar plugins a ciertos componentes, sin que estos sepan los detalles de la implementación de los mismos.

Por ejemplo, un sistema integrado de desarrollo (IDE) permite ampliar su funcionamiento mediante plugins, tales como: Lite Server, Remote SSH, etc., esto sin afectar el funcionamiento del mismo. El IDE permite agregar plugins siempre y cuando cada extensión siga las reglas definidas para su implementación. Es decir, el sistema se encuentra abierto para su extensión mediante plugins pero no para la modificación de las funciones centrales del IDE.

Otro ejemplo del principio de abierto/cerrado son las librerías de software. Estas se desarrollan con el objetivo que sean utilizadas en diferentes contextos e implementadas en todos los casos posibles, es decir, las librerías por naturaleza deben de estar abiertas para su extensión. Sin embargo, un cambio en el core de la librería podría significar cambios en todos los puntos y contextos donde dicha librería ha sido utilizada, en otras palabras, las librerías por naturaleza deben de estar cerradas para modificaciones.

Veamos un ejemplo a nivel de código. Actualmente laboras en una empresa de desarrollo de juegos de PC. Tu jefe, como parte del próximo proyecto, te solicita implementar una librería que permita el cálculo del área de rectángulos. Esta será útil para la maquetación de los personajes del juego.

Para llevar a cabo esta tarea defines las siguientes clases:

public class Rectangulo{

    private final double base;
    private final double altura;

    public Rectangulo(double base, double altura){
        this.base = base; 
        this.altura = altura;
    }

    /*Getters / Setters*/
}
Enter fullscreen mode Exit fullscreen mode

La clase Rectangulo almacena la base y altura de la figura geométrica.

public class CalculadorArea{

    public double calcularArea(Rectangulo rectangulo){
        return rectangulo.getBase * rectangulo.getAltura;
    }   
}
Enter fullscreen mode Exit fullscreen mode

La clase CalculadorArea define el método encargado de calcular el área del rectángulo, el cual se realiza mediante la multiplicación de la base por la altura.

El calculador de areas se utiliza de la siguiente forma:

package com.yourregulardeveloper.main;

public class App{

    public static void main(String[] args){

        CalculadorArea calcArea = new CalculadorArea();
        Rectangulo rec1 = new Rectangulo(13.5, 14);
        Rectangulo rec2 = new Rectangulo(7.89, 9.85);

        System.out.println("Calculo de rectangulo 1: " + calcArea.calcularArea(rec1));
        System.out.println("Calculo de rectangulo 2: " + calcArea.calcularArea(rec2));
    }
}
Enter fullscreen mode Exit fullscreen mode

Tu librería termina siendo un éxito y es utilizada en varios proyectos adicionales que está llevando a cabo la empresa. Meses después tu jefe te solicita que la librería también calcule el área de cuadrados.

Al momento de empezar a diseñar tu implementación te das cuenta de dos factores:

  • El cuadrado es un caso especial de rectángulo.
  • El área del cuadrado se calcula de forma diferente al rectángulo tradicional. Está se calcula elevando el tamaño de sus lados al cuadrado.

Estos factores resaltan que el código de tu librería no cumple con el principio de abierto/cerrado. Por supuesto que podrías debatir que en la clase Rectangulo se puede asignar el mismo valor para la base/altura y se obtendría el mismo funcionamiento para las figuras de tipo cuadrado ¿Pero qué sucede si en futuros requerimientos se solicita el cálculo del área de un circulo o de cualquier otra figura geométrica?

La respuesta es la implementación del principio abierto/cerrado.

Aplicación del principio de abierto/cerrado

El caso anterior nos ejemplifica los problemas que se pueden generar al momento de diseñar aplicaciones que no tomen en cuenta los cambios que se pueden dar en ciertos módulos. El objetivo principal del principio de abierto/cerrado es exactamente ese, diseñar soluciones que tomen en cuenta los cambios que se puedan dar en el futuro y estructurar las soluciones para que se puedan agregar dichos cambios sin afectar el código existente.

Por ejemplo, para solucionar el caso de los rectángulos nos vamos ayudar del uso de interfaces. Una interfaz es un conjunto de métodos que definen el funcionamiento de una clase, pero no su implementación. Por lo tanto, podemos definir una interfaz genérica para cualquier figura geométrica y que tenga la definición de un método para el cálculo del área. Luego, que cada figura geométrica defina la implementación propia de cada cálculo de área.

Tomar en cuenta la siguiente interfaz:

public interface FiguraGeometrica{
      public double calcularArea();
}
Enter fullscreen mode Exit fullscreen mode

Luego se crean las clases de cada figura geométrica que se utilizará en la librería. Cada clase del tipo FiguraGeometrica implementa su propia forma de calcular el área.

public class Rectangulo implements FiguraGeometrica{

    private final double base;
    private final double altura;

    public Rectangulo(double base, double altura){
        this.base = base;
        this.altura = altura;
    }

    @Override
    public double calcularArea(){
        return this.base * this.altura;
    }
}
Enter fullscreen mode Exit fullscreen mode
public class Cuadrado implements FiguraGeometrica{

    private final double lado;

    public Cuadrado(double lado){
        this.lado = lado;
    }

    @Override
    public double calcularArea(){
        return this.lado * this.lado;
    }
}
Enter fullscreen mode Exit fullscreen mode
public class Circulo implements FiguraGeometrica{

    public static final double PI = 3.1416;
    private final double radio;

    public Circulo(double radio){
        this.radio = radio;
    }

    @Override
    public double calcularArea(){
        return (Circulo.PI * (this.radio * this.radio));
    }
}
Enter fullscreen mode Exit fullscreen mode

Se modifica la clase CalculadorArea para que reciba un objeto del tipo FiguraGeometrica y con este calcula el área de la figura, sin importar de cual se trata.

public class CalculadorArea {

    public double calcularArea(FiguraGeometrica figura){
        return figura.calcularArea();
    }    
}
Enter fullscreen mode Exit fullscreen mode

La ejecución de la librería se puede realizar de la siguiente forma.

public class App {

    public static void main(String[] args) {

        CalculadorArea calcArea = new CalculadorArea();
        Cuadrado cuadrado = new Cuadrado(3.15);
        Rectangulo rectangulo = new Rectangulo(7.85, 10.85);
        Circulo circulo = new Circulo(7.98);

        System.out.println("Calculo de figura: " + calcArea.calcularArea(cuadrado));
        System.out.println("Calculo de figura: " + calcArea.calcularArea(rectangulo));
        System.out.println("Calculo de figura: " + calcArea.calcularArea(circulo));
    }
}
Enter fullscreen mode Exit fullscreen mode

La nueva estructura de la librería cumple con el principio de abierto/cerrado, ya que se pueden agregar más figuras geométricas sin modificar el código actual, siempre y cuando las nuevas figuras implementen la interfaz FiguraGeometrica.

¿Cuando se puede aplicar el principio de abierto/cerrado?

El principio abierto/cerrado no debe de ser una regla inamovible en el diseño de soluciones de software. El problema con este principio, al igual que con el de la responsabilidad única, es que el desarrollador debe de predecir los posibles futuros requerimientos que generaran cambios en el código actual.

Esto puede ser más sencillo para desarrolladores senior o semi-senior, pero para desarrolladores junior puede ser una receta para el desastre. La incorrecta aplicación del principio de abierto/cerrado puede complicar innecesariamente el diseño del código. Esto en lugar ayudar a que la aplicación sea más mantenible y escalable, logra exactamente lo opuesto.

Mi recomendación personal es siempre escribir código que sea fácilmente entendible por otros desarrolladores y fácil de modificar cuando cambien los requerimientos. En relación con el principio de abierto/cerrado es mejor esperar la primera iteración de cambios para poder prever los puntos en donde el diseño cambiará y así poder aplicar el principio.

Si deseas ampliar tu conocimiento acerca del principio de abierto/cerrado, puedes leer el blog de Robert C. Martin.

En una proxima publicación ahondaremos en el principio de sustitución de Liskov.

Discussion (0)