This post is also available in english

Introducción

Este principio establece que una entidad (clase, módulo, función, etc) debe quedar abierta para su extensión pero cerrada para su modificación.

Debemos ser capaces de extender el comportamiento de nuestras entidades, sin necesidad de modificar su código.

Cuando hablamos de que una entidad debe estar abierta para su extensión, nos referimos a que esta debe tener la capacidad de adaptarse a los cambios y necesidades de una aplicación. Si hablamos de la otra parte, cerrada para su modificación, nos da a entender que la adaptabilidad de dicha entidad no debe darse como resultado de la modificación del core de la misma.

Aplicar este concepto de forma correcta nos brinda un software más fácil de mantener y ampliar funcionalidades al minimizar los cambios en la base del código y sin modificar partes básicas de la aplicación. También evita generar nuevos errores en funcionalidades que ya estaban desarrolladas, probadas y funcionando correctamente.

Cómo podemos detectar que estamos violando dicho principio?

Si cada vez que hay un nuevo requisito o una modificación de los existentes, las mismas entidades se ven afectadas, podemos empezar a identificar una violación de este principio

Ejemplo

Para nuestro ejemplo vamos a usar una clase Vehículo, la cual debe tener la capacidad de dibujar al tipo de vehículo que se le pase por parámetro.

Para esto, contamos con las siguientes estructuras:

class Vehicle(val type: VehicleType)

enum class VehicleType {
  CAR, MOTORBIKE
}

Veamos el método encargado de realizar la funcionalidad mencionada anteriormente:

fun draw(vehicle: Vehicle) {
  when(vehicle.type) {
    VehicleType.CAR -> drawCar(vehicle)
    VehicleType.MOTORBIKE -> drawMotorbike(vehicle)
  }
}

Con la estructura planteada, cada vez que necesitemos agregar un nuevo vehículo implicaría crear un nuevo enum, un nuevo case para dicho enum, y un nuevo método para implementar el dibujado del nuevo caso. De esta manera estaríamos violando el principio Open-Closed, ya que en cada nuevo caso necesitariamos modificar el código base de la clase Vehículo.

Vamos a ver cómo solucionamos este problema mediante polimorfismo, haciendo que cada enum sea una clase concreta y sepa como dibujarse a si misma.

Para esto, convertimos la clase Vehículo en una interfaz con el método draw y cada enum en una clase concreta, que implemente nuestra interfaz:

interface Vehicle {
  fun draw()
}
class Car : Vehicle {
  override fun draw() {
    // Draw the car
  }
}
class Motorbike : Vehicle {
  override fun draw() {
    // Draw the motorbike
  }
}

De esta manera, nuestro método anterior se reduce a:

fun draw(vehicle: Vehicle) {
  vehicle.draw()
}

Si volvemos al caso anterior, en el cual teniamos la necesidad de agregar nuevos requisitos, ahora podemos hacerlo sin tocar el código base de Vehículo. Añadir nuevos vehiculos es tan sencillo como crear la clase correspondiente y hacer que implemente nuestra interfaz:

class Truck: Vehicle {
  override fun draw() {
    // Draw the truck
  }
}

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.