Seguramente muchos escuchamos hablar sobre los principios SOLID para crear diseños orientados a objetos: son una excelente guía general de 5 principios que, si seguimos, nos facilitarán la creación de sistemas mantenibles y flexibles.
Lo interesante de los principios SOLID es que aplican a cualquier lenguaje orientado a objetos, ya que son buenas prácticas de diseño probadas en el tiempo. Vamos a repasarlos!
¿Qué es SOLID?
SOLID es un acrónimo para 5 principios de diseño de objetos, que lo usó por primera vez Michael Feathers en base a los "primeros cinco principios" de diseño y programación enunciados por Robert Martin en el año 2000. Estos principios se aplican todos juntos, y ayudan a que los sistemas sean más mantenibles y fáciles de extender en el tiempo. Se pueden aplicar los principios SOLID en código existente como guía para realizar un refactor, y durante la misma codificación junto con prácticas como TDD o Pair Programming.
SOLID es un acrónimo en inglés de:
- S: Principio de Responsabilidad Única (Single Responsability Principle)
"Un objeto debe tener una única responsabilidad" - O: Principio de Abierto / Cerrado (Open / Closed Principle)
"las entidades de software deben ser abiertas a la extensión y cerradas a la modificación" - L: Principio de Sustitución de Liskov (Liskov Substitution Principle)
"los objetos de un programa deben poder reemplazarse por instancias de sus subtipos sin alterar la correctitud del programa" - I: Principio de Segregación de Interfaces (Interface Segregation Principle)
"es preferible muchas interfaces específicas de cliente que una interfaz de uso general" - D: Principio de Inversión de Dependencias (Dependency Inversion Principle)
"debemos depender de las abstracciones y no de las concreciones"
S: Principio de Responsabilidad Única
Este principio de diseño de objetos indica que cada clase debe tener una única resposanbilidad, y dicha responsabilidad debe estar encapsulada por completo por la clase. Todos los servicios de la clase deben estar estrictamente alineados a esta responsabilidad.
Se define como responsabilidad al motivo de cambio, por lo que una clase o módulo debería tener una, y solo una, razón para cambiar. Por ejemplo, pensemos en un módulo que compila e imprime un reporte. Este módulo puede cambiarse por dos razones: primero, puede cambiar el contenido del reporte, y segundo, puede cambiar el formato del reporte. Estos dos cambios son causas muy distintas; una es en esencia, y la otra en cosmética. El principio de responsabilidad única dice que estos dos aspectos del problema en realidad son dos responsabilidades diferentes, y por lo tanto deberían existir en clases o módulos separados. Genera un mal diseño acoplar dos cosas que cambiar por motivos distintos en momentos distintos.
O: Principio de Abierto / Cerrado
Este principio indica que una entidad de software (clase, módulo, función, etc.) debe ser abierta a extensiones y cerrada a modificaciones. Es decir, que esta entidad permite alterar su comportamiento sin modificar su código fuente. Esto se torna especialmente importante en entornos productivos, donde se suelen necesitar revisiones de código, pruebas unitarias y otros procedimientos para asegurar su funcionamiento. El código que sigue este principio no cambia cuando se extiende, por lo que se simplifica el esfuerzo.
Este principio se define con dos enfoques distintos: el de Meyer, y el polimórfico.
Bertrand Meyer es quien introdujo este principio en 1988. Su idea era que una vez que una clase estaba completa, sólo debía modificarse para corregir errores; nuevas características o cambios deberían realizarse en una clase nueva. Esta clase podía reusar el código de la original a través de la herencia.
La definición de Meyer hace uso de la implementación por herencia. La implementación puede reutilizarse a través de la herencia, aunque pueden cambiarse las especificaciones de la interfaz. La implementación existente está cerrada a modificaciones, y las nuevas implementaciones no necesitan implementar la interfaz existente.
Durante 1990, el principio de Abierto / Cerrado se redefinió para referirse al uso de las interfaces abstractas, en donde las implementacionse pueden cambiarse y se pueden crear múltiples implementaciones y sustituirlas polimórficamente entre si.
A diferencia del uso de Meyer, esta definición fomenta la herencia de clases base abstractas. Se puede reutilizar la especificación de las interfaces a través de la herencia, aunque podría no reutilizarse las implementaciones. La interfaz existente es cerrada a modificaciones, y las nuevas implementaciones deben, mínimamente, implementar esta interfaz.
L: Principio de Sustitución de Liskov
Este principio indica que, en un programa de computación, si S es un subtipo de T, entonces los objetos de tipo T pueden ser reemplazados por objetos de tipo S sin alterar ninguna de las propiedades deseables del programa.
I: Principio de Segregación de Interfaces
Este principio indica que ningún cliente debería verse forzado a depender en métodos que no utiliza. El principio separa las interfaces que son muy grandes en interfaces más pequeñas y específicas, de manera que los clientes sólo tengan que conocer los métodos que les interesan. El objetivo del principio de Segregación de Interfaces es mantener un bajo acoplamiento en el sistema y, por lo tanto, hacer que sea más facil realizar refactors, cambios y redespliegues.
D: Principio de Inversion de Dependencias
Este principio de la orientación a objetos se refiere a un tipo particular de desacoplamiento, en donde se invierte la relación tradicional de dependencia establecida entre los módulos de alto nivel y los de bajo nivel. Este principio indica que:
A. Los módulos de nivel superior no deben depender en módulos de niveles inferiores. Ambos deben depender de abstracciones.
B. Las abstracciones no deben depender de detalles. Los detalles deben depender de abstracciones.
Este principio invierte la forma en que las personas piensan sobre el diseño de objetos, ya que tanto los objetos de nivel superior como los inferiores dependen de abstracciones.
En las arquitecturas de aplicación convencionales, se diseña a los componentes de bajo nivel para que sean consumidos por compoenntes de alto nivel, quienes a su vez permiten construir sistemas más complejos. En esta composición, los componentes de alto nivel dependen directamente de los componentes de bajo nivel para realizar sus tareas. Esta dependencia con los componentes de bajo nivel limitan las oportunidades de reutilización de los componentes de alto nivel.
El objetivo del Principio de Inversión de Dependencias es desacoplar a los componentes de alto nivel de los componentes de bajo nivel, de manera que sea posible la reutilización de los componentes de alto nivel al usar componentes de bajo nivel distintos. Esto se logra separando a los componentes de alto y bajo nivel en paquetes/librerías separadas, en donde las interfaces que definen el comportamiento requerido por un componente de alto nivel existen en el paquete de este componente de alto nivel. Un componente de bajo nivel necesitará depender del componente de alto nivel (por la interfaz), y de ahí que se "invierte" la relación tradicional de dependencias.
Existen otros patrones de diseño (como Plugin, Service Locator y Dependency Injection) que facilitan la creación de estos componentes de bajo nivel y asignación a los componentes de alto nivel.