Principio Open/Closed (SOLID 2ª parte)
Antonio Leiva

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:

Si quieres tenerlo más cómodo, puedes descargarte el contenido en formato PDF y leerlo donde quieras. Te he preparado esta guía de Principios SOLID para ti.

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:

class Vehicle(val type: VehicleType) 

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

enum class VehicleType{
    CAR, MOTORBIKE
}

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

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

Mientras no necesitemos dibujar más tipos de vehículos ni veamos que este when 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 o polimorfismo, el paso evidente es sustituir ese enumerado por clases reales, y que cada clase sepa cómo pintarse:

interface Vehicle {
    fun draw()
}

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

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

Ahora nuestro método anterior se reduce a:

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

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

class Truck: Vehicle {
    override fun 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 MotorbikeDrawer que 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.

Y si lo prefieres, puedes descargarte lo que estamos viendo aquí en esta guía de Principios SOLID de forma gratuita.

Quizá también te interese…

Unidirectional Data Flow: Qué es y cómo funciona en Android

Unidirectional Data Flow: Qué es y cómo funciona en Android

Hay algunos conceptos que están empezando a resonar muy fuerte en Android, y Unidirectional Data Flow es uno de ellos. Como puede que sepas si estás apuntado a la newsletter, me he planteado escribir una serie de artículos sobre MVI (Model View Intent), y me he topado...

7 Comentarios

  1. Joaquin Engelmo Moriche

    Hola Antonio!

    Antes de nada gracias por el esfuerzo de este proyecto, mola 🙂

    Ha explicado muy bien el concepto con el ejemplo, yo habría creado un ejemplo 100% basado en interfaces sin usar clases abstractas, pero como bien dices todas las alternativas tienen sus cosas. Confío en que vas a escribir un artículo sobre el famoso tema de abstracciones vs interfaces y me quedo contento 😉

    Mucho ánimo! Un abrazo

    Responder
    • Antonio Leiva

      Gracias Kini! Pues no lo había pensado pero es verdad que es un buen tema, me lo apunto. O si quieres escribirlo tú, por mi encantado 🙂 Espero tenerte algún día por aquí como blogger invitado, hablando de testing por ejemplo si te apetece. Un abrazo!

      Responder
  2. Sebas LG

    Buena explicación del principio. Éste es para mi gusto el más utópico de los 5 principios SOLID.
    Me ha gustado mucho el matiz de que el ejemplo incumple el SRP, porque justo ese ejemplo de la función ‘draw’ lo he visto muchísimas veces en diferentes proyectos.
    ¿Tienes algún enlace con más información o ejemplos de soluciones como la que mencionas con objectos ThingDrawer? Estoy curioso de cómo implementan los interfaces. Voy a buscar si encuentro algo en google.

    Responder
    • kinisoftware

      Hola Sebas,

      me voy a meter donde no me llaman 😀 Puedes echarle un vistazo al patrón Decorator que te ayuda a seguir este principio de Open/Close https://sourcemaking.com/design_patterns/decorator y supongo que por ahí van los tiros de Antonio sobre el ejemplo.

      Si me estoy pasando de frenada también me lo podéis decir 😀

      Responder
      • Antonio Leiva

        ¡Para nada! Toda ayuda es buena, y todos aprendemos de todos. Muchas gracias por el enlace. Efectivamente, creo que ese enlace ayudará a Sebas a aclarar un poco el tema.

        Responder
  3. Sebas LG

    Gracias por la recomendación, muy interesante el patrón y la página de patrones con ejemplos.
    Creo que mi problema en realidad era más semántico porque no me imaginaba dónde poner el CarDrawer, en mi imaginación simplemente renombrando la clase CarDrawer a DrawableCar el ejemplo ya tiene sentido para mi, tienes una colección de Drawables que implementan el método draw() y son construidos con un objecto de la familia Vehicle.

    Responder

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de Antonio Leiva Gordillo, con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Acepto la política de privacidad *