El Principio Abierto Cerrado es responsable de la ‘O’ de los principios S.O.L.I.D. Originalmente acuñado por Bertrand Meyer, establece que:
Las entidades de software (clases, módulos, funciones, …) deben estar abiertas para extensión pero cerradas para modificación
Parece un principio increíblemente simple, pero está detrás de la mayoría de las mejores prácticas que usamos al diseñar software. Está relacionado con una de las metas principales de la arquitectura de software: la proporción entre cambios en los requerimientos y cambios en el código. Un pequeño cambio en los requerimientos debería cumplirse sin mucho esfuerzo, mientras que funcionalidades más grandes aún deberían ser posibles de implementar.
El principio tiene un solo objetivo: hacer que el sistema sea fácil de extender sin impactar el código que ya existe.
¿Por qué abierto? ¿Por qué cerrado?
En el libro Object-Oriented Software Construction (1988), por Bertrand Meyer, aparecen las siguientes dos definiciones:
Se dirá que un módulo está abierto si aún está disponible para extensión. Por ejemplo, debería ser posible agregar campos a las estructuras de datos que contiene, o nuevos elementos al conjunto de funciones que realiza.
Se dirá que un módulo está cerrado si está disponible para uso por otros módulos. Esto asume que al módulo se le ha dado una descripción bien definida y estable (la interfaz en el sentido de ocultamiento de información)
Queremos módulos que incorporen las dos definiciones:
-
Queremos que los módulos estén abiertos a extensión. Si los requerimientos cambian y recibimos solicitudes para nuevas funcionalidades, nos gustaría acomodarlas en nuestro código. Queremos la habilidad de extender el comportamiento de nuestra aplicación para que podamos adaptarnos a las necesidades de nuestros clientes.
-
Queremos que los módulos estén cerrados para modificaciones, ya que cada cambio tiene el potencial de tener efectos secundarios negativos. Si creamos las abstracciones correctas, podemos depender principalmente de comportamientos bien definidos y estables.
Si un módulo está cerrado para modificación y abierto para extensión, obtenemos lo mejor de dos mundos. Nuestro código es tanto flexible como estable, permitiendo que nuestra aplicación crezca de manera controlada y estructurada.
Esas dos ideas parecen difíciles de reconciliar, después de todo cuando necesitamos agregar una funcionalidad o realizar un cambio, usualmente abrimos una clase para agregar métodos o modificar los ya existentes. Este es exactamente el comportamiento contra el que se opone este principio. En su lugar, podemos agregar nuevo comportamiento escribiendo código nuevo, no modificando el código bien probado que ya funciona.
Reportes cerrados, reportes abiertos
Imagina que tenemos una clase Report con algún comportamiento básico y la habilidad de almacenar sus contenidos en una base de datos usando el método save.
class Report
def initialize()
#...
end
def add_info
#...
end
def set_author
#...
end
def genereate_footers
#...
end
def save
# Almacena el reporte en una base de datos
end
end
Como nuevo requerimiento, necesitamos poder almacenar los contenidos de nuevas maneras:
- En un archivo de texto en la máquina local.
- En un contenedor de red.
- En un caché (como Redis).
Primer intento de solución: Más métodos
Empezamos a pensar en una buena manera de acomodar estos cambios y lo primero que viene a la mente es crear nuevos métodos:
- crear un método store_in_textfile
- crear un método store_in_network
- crear un método store_in_redis
Funciona, pero hay un problema: cuando la gerencia solicite nuevas maneras de guardar los reportes, necesitaremos abrir la clase Report y modificarla. Necesitaremos lidiar con los problemas asociados con violar la cláusula cerrado para modificación del OCP.
Entonces, necesitamos encontrar una manera de extender el comportamiento sin modificar Report cada vez que surja un nuevo requerimiento como este.
Segundo intento de solución: Usar herencia
Recordamos que estamos usando un lenguaje OO basado en clases y decidimos usar herencia clásica (clásica como en basada en clases). Podemos heredar de Report y crear subclases basadas en el método de almacenamiento:
Esto parece estar bien. Después de todo, no necesitamos abrir la clase Report original para agregar nuevo comportamiento a la aplicación. Todo lo que necesitamos es crear una nueva subclase para cada requerimiento de almacenamiento diferente y terminamos.
Aun así, la herencia podría no ser la solución perfecta para este problema. Podrías argumentar que un ReportDB es-un tipo de Report, pero semánticamente no tiene mucho sentido. ¿Qué pasaría si, por ejemplo, quisiéramos crear diferentes tipos de reporte basados en una característica diferente, algo distinto a la manera en que se almacena?
Supón que empezamos a crear subclases de nuevos reportes basados en idioma y necesitáramos soporte para inglés, español y portugués. Como necesitan soporte diferente para caracteres y otras regulaciones imaginarias, necesitaríamos las siguientes clases:
Clases con soporte para el idioma inglés:
- ReportDbEnglish
- ReportTextfileEnglish
- ReportNetworkEnglish
- ReportRedisEnglish
Clases con soporte para el idioma español:
- ReportDbSpanish
- ReportTextfileSpanish
- ReportNetworkSpanish
- ReportRedisSpanish
Clases con soporte para el idioma portugués:
- ReportDbPortuguese
- ReportTextfilePortuguese
- ReportNetworkPortuguese
- ReportRedisPortuguese
Esto ejemplifica qué tan fácil es caer en la trampa de explosión de subclases más adelante, incluso si la solución funciona bien ahora. Echemos un vistazo a otra solución, una que respeta el OCP.
Tercera solución: Aislar lo que cambia
Actualmente, la clase Report tiene el conocimiento necesario para almacenar información. Después de recibir más y más solicitudes para nuevas maneras de almacenar información, sabemos que este comportamiento probablemente cambie.
Con esto en mente, podemos crear una nueva abstracción para almacenar información, diferentes clases implementarán el comportamiento concreto para bases de datos, archivos de texto y otros. Esas clases serán inyectadas en instancias de la clase Report.
La clase Report ahora tiene la siguiente forma:
class Report
attr_accessor :information_saver
def initialize(report_information_saver)
@information_saver = report_information_saver
#...
end
#...
def save
information_saver.save_info(self)
end
end
report_information_server, como abstracción, tiene solo un método save_info que recibe un objeto de tipo Report. Los detalles de implementación se dejan a las clases que implementan esta abstracción. Al final, tendrás la siguiente jerarquía de clases:
Este enfoque tiene varias ventajas:
- La responsabilidad de almacenar información pertenece a su propia clase, Report ahora está más alineado con el SRP.
- Podemos agregar nuevo comportamiento sin necesidad de abrir ninguna de las clases nuevas, solo necesitamos una nueva subclase de report_information_server.
- Si necesitamos nuevas combinaciones de Report (como el ejemplo de idiomas de arriba), solo necesitamos crear clases para esas características e inyectarlas, previniendo la explosión de clases.
- Tenemos una solución que será más fácil de entender y mantener a largo plazo.
Nuestra tercera solución tiene un nombre: es el Patrón Strategy. La mayoría de los patrones de diseño son en realidad maneras de organizar tu código para que se siga el principio OCP. Podríamos estudiar algunos de los patrones de diseño más populares en artículos futuros.
Un principio en el corazón de la programación orientada a objetos
El principio Abierto Cerrado es uno de los principios de diseño más importantes. Seguirlo te permite usar las mejores características de la programación orientada a objetos, y te permite crear aplicaciones modulares y fáciles de mantener. Usar características OO como herencia no es suficiente, necesitas definir explícitamente los límites y abstracciones en tu aplicación basándote en tus requerimientos y experiencia en diseño.
En este artículo, exploramos solo una manera de implementar el principio OCP, pero hay incontables otras técnicas a tu disposición. Como mencionamos antes, los patrones de diseño son una manera popular de asegurar que tu aplicación esté cerrada para modificación y abierta para extensión.
Como último pensamiento, recuerda que es imposible hacer un programa completamente cerrado. En un proyecto real, no puedes proteger toda tu aplicación de modificación, está destinado a pasar tarde o temprano.
Lo que puedes elegir es qué cerrar y qué dejar abierto. El truco es identificar las cosas que tienen más probabilidad de cambiar y construir las abstracciones correctas para asegurar que podemos agregar nuevo comportamiento sin modificar el código.
La calidad de los límites que dibujas depende de tu conocimiento del dominio y habilidades de diseño. Toma práctica, así que no tengas miedo de construir tus propias abstracciones y ver qué funciona y qué no.
Qué hacer a continuación
- Comparte este artículo con amigos y colegas. Gracias por ayudarme a llegar a gente que podría encontrar útil esta información.
- Lee el artículo sobre el OCP del Tío Bob.
- Hay más información sobre el OCP en el capítulo 8 de Clean Architecture.
- Envíame un email con preguntas, comentarios o sugerencias (está en la página Autor).