Después de haber echado un vistazo al primer principio, el principio de responsabilidad única, es el momento de hablar del principio Open/Closed, el segundo en la lista de SOLID:

Principio Open/Closed

El principio Open/Closed fue nombrado por primera vez por Bertrand Mayer, un programador francés, quien lo incluyó en su libro Object Oriented Software Construction allá por 1988.

Este principio nos dice que una entidad de software debería estar abierta a extensión pero cerrada a modificación. ¿Qué quiere decir esto? Que tenemos que ser capaces de extender el comportamiento de nuestras clases sin necesidad de modificar su código. Esto nos ayuda a seguir añadiendo funcionalidad con la seguridad de que no afectará al código existente. Nuevas funcionalidades implicarán añadir nuevas clases y métodos, pero en general no debería suponer modificar lo que ya ha sido escrito.

La forma de llegar a ello está muy relacionada con el punto anterior. Si las clases sólo tienen una responsabilidad, podremos añadir nuevas características que no les afectarán. Esto no quiere decir que cumpliendo el primer principio se cumpla automáticamente el segundo, ni viceversa. Luego verás un caso claro en el ejemplo.

El principio Open/Closed se suele resolver utilizando polimorfismo. En vez de obligar a la clase principal a saber cómo realizar una operación, delega esta a los objetos que utiliza, de tal forma que no necesita saber explícitamente cómo llevarla a cabo. Estos objetos tendrán una interfaz común que implementarán de forma específica según sus requerimientos.

¿Cómo detectar que estamos violando el principio Open/Closed?

Una de las formas más sencillas para detectarlo es darnos cuenta de qué clases modificamos más a menudo. Si cada vez que hay un nuevo requisito o una modificación de los existentes, las mismas clases se ven afectadas, podemos empezar a entender que estamos violando este principio.

Ejemplo

Siguiendo con nuestro ejemplo de vehículos, podríamos tener la necesidad de dibujarlos en pantalla. Imaginemos que tenemos una clase con un método que se encarga de dibujar un vehículo por pantalla. Por supuesto, cada vehículo tiene su propia forma de ser pintado. Nuestro vehículo tiene la siguiente forma:

public class Vehicle {

    public VehicleType getType(){
        ...
    }
    ...
}

Básicamente es una clase que especifica su tipo mediante un enumerado. Podemos tener por ejemplo un enum con un par de tipos:

public enum VehicleType {
    CAR,
    MOTORBIKE
}

Y éste es el método de la clase que se encarga de pintarlos:

public void draw(Vehicle vehicle) {
    switch (vehicle.getType()) {
        case CAR:
            drawCar(vehicle);
            break;
        case MOTORBIKE:
            drawMotorbike(vehicle);
            break;
    }
}

Mientras no necesitemos dibujar más tipos de vehículos ni veamos que este switch se repite en varias partes de nuestro código, en mi opinión no debes sentir la necesidad de modificarlo. Incluso el hecho de que cambie la forma de dibujar un coche o una moto estaría encapsulado en sus propios métodos y no afectaría al resto del código.

Pero puede llegar un punto en el que necesitemos dibujar un nuevo tipo de vehículo, y luego otro… Esto implica crear un nuevo enumerado, un nuevo case y un nuevo método para implementar el dibujado. En este caso sería buena idea aplicar el principio Open/Closed.

Si lo solucionamos mediante herencia y polimorfismo, el paso evidente es sustituir ese enumerado por clases reales, y que cada clase sepa cómo pintarse:

public abstract class Vehicle {

    ...

    public abstract void draw();
}

public class Car extends Vehicle {

    @Override public void draw() {
        // Draw the car
    }
}

public class Motorbike extends Vehicle {

    @Override public void draw() {
        // Draw the motorbike
    }
}

Ahora nuestro método anterior se reduce a:

public void draw(Vehicle vehicle) {
    vehicle.draw();
}

Añadir nuevos vehículos ahora es tan sencillo como crear la clase correspondiente que extienda de Vehicle:

public class Truck extends Vehicle {

    @Override public void draw() {
        // Draw the truck
    }
}

Como puedes ver, este ejemplo choca directamente con el que vimos en el Principio de Responsabilidad Única. Esta clase está guardando la información del objeto y la forma de pintarlo. ¿Implica eso que es incorrecto? No necesariamente, tendremos que ver si el hecho de tener el método draw en nuestros objetos afecta negativamente la mantenibilidad y testabilidad del código. En ese caso habría que buscar alternativas.

Aunque no la voy a presentar aquí, una alternativa para cumplir ambos sería aplicar este polimorfismo a clases que sólo tengan un método de pintado y que reciban el objeto a pintar por constructor. Tendríamos por tanto un CarDrawer que se encargue de pintar coches o un MotorbikeDrawerque dibuje motos, todos ellos implementando draw(), que estaría definido en una clase o interfaz padre.

¿Cuándo debemos cumplir con este principio?

Hay que decir que añadir esta complejidad no siempre compensa, y como el resto de principios, sólo será aplicable si realmente es necesario. Si tienes una parte de tu código que es propensa a cambios, plantéate hacerla de forma que un nuevo cambio impacte lo menos posible en el código existente. Normalmente esto no es fácil de saber a priori, por lo que puedes preocuparte por ello cuando tengas que modificarlo, y hacer los cambios necesarios para cumplir este principio en ese momento.

Intentar hacer un código 100% Open/Closed es prácticamente imposible, y puede hacer que sea ilegible e incluso más difícil de mantener. No me cansaré de repetir que las reglas SOLID son ideas muy potentes, pero hay que aplicarlas donde corresponda y sin obsesionarnos con cumplirlas en cada punto del desarrollo. Casi siempre es más sencillo limitarse a usarlas cuando nos haya surgido la necesidad real.

Conclusión

El principio Open/Closed es una herramienta indispensable para protegernos frente a cambios en módulos o partes de código en los que esas modificaciones son frecuentes. Tener código cerrado a modificación y abierto a extensión nos da la máxima flexibilidad con el mínimo impacto.

¿Conocías este principio? ¿En qué situaciones te ha resultado de utilidad?

El siguiente artículo tratará sobre el Principio de Sustitución de Liskov, el tercero de los 5 principios SOLID.