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:
- ¿Qué son los Principios SOLID?
- Principio de Responsabilidad Única
- Principio Open/Closed
- Principio de Sustitución de Liskov
- Principio de Segregación de Interfaces
- Principio de Inversión de Dependencias
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.
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
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!
Será un placer colaborar contigo en lo que sea 😉
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.
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 😀
¡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.
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.