Principios S.O.L.I.D

SOLID

Los principios S.O.L.I.D, como grupo, fueron recopilados, explicados y popularizados por Robert C. Martin hace aproximadamente 20 años. Sus primeros artículos sobre ellos circularon a finales de los años 90 y los principios fueron cubiertos extensamente en su libro “Desarrollo de Software Ágil: Principios, Patrones y Prácticas”, publicado en 2002. Constituyen otro intento de crear una teoría de programación, un conjunto de principios para guiar cómo escribimos y estructuramos el código. S.O.L.I.D es un acrónimo formado por la primera letra de cada principio:

  • S (Principio de Responsabilidad Única)
  • O (Principio Abierto/Cerrado)
  • L (Principio de Sustitución de Liskov)
  • I (Principio de Segregación de la Interfaz)
  • D (Principio de Inversión de Dependencias)

El beneficio de S.O.L.I.D es que estos son principios más concretos que las ideas abstractas de acoplamiento y cohesión. Nos permiten apuntar a reducir el acoplamiento y aumentar la cohesión a través de métodos más prácticos.

Estos principios son simples en su formulación, pero son extremadamente profundos en su aplicación. Simplemente conocer la definición no es suficiente para incorporar las ideas en lo que hacemos; necesitamos realmente involucrarnos con estos principios, discutirlos mientras hacemos nuestro trabajo y especialmente mientras practicamos deliberadamente, ya que de esta manera podremos utilizar estos principios de manera efectiva.

Conocer cada principio de forma aislada es bueno. Pero aplicar todos estos principios como un bloque sólido es cuando realmente se vuelven poderosos.

El Principio de Responsabilidad Única

“Una clase debería tener solo una razón para cambiar”.

Robert C. Martin reconoce que este principio es muy similar al concepto de cohesión desarrollado por muchos otros autores desde finales de los años 70, pero su formulación es sutilmente diferente. En lugar de pensar en la cohesión de una clase y aplicar varios análisis para calcular una puntuación de cohesión, él dice que si hay más de una razón para que una clase cambie, entonces está haciendo demasiado y debería dividirse en dos clases, cada una haciendo una sola cosa.

Aunque esto suena bastante simple, también es probablemente el más difícil de estos principios de llevar a la práctica. No siempre conocemos la forma en que las cosas van a cambiar, por ejemplo. No es una buena idea segregar cosas que nunca van a cambiar porque entonces estamos haciendo un trabajo adicional que es innecesario. Por lo tanto, necesitamos pensar y posiblemente predecir la forma en que las cosas pueden cambiar en el futuro. Esto es difícil, y es probable que no lo hagamos bien la mayor parte del tiempo. Luego, un año después, alguien preguntará “¡¿por qué no separaste esta parte de la clase – no seguiste el SRP?!”

La clave aquí es darse cuenta cuando se está cambiando una clase de más de una manera y buscar separar las partes que están cambiando. Una forma de descubrir qué clases están cambiando es usar el historial de control de origen para generar un mapa de calor de su código: las clases que se están modificando a menudo durante un largo período probablemente estén haciendo demasiado (también podrían estar violando el Principio Abierto/Cerrado).

El Principio Abierto/Cerrado

“Las entidades de software (clases, módulos, funciones, etc.) deberían estar abiertas para su extensión pero cerradas para su modificación”.

Este principio seguido al extremo significaría que cada vez que queramos agregar funcionalidad, creamos una nueva clase o una nueva función y nunca cambiamos el código existente. Por supuesto, este no es un sueño realista, pero es una idea hacia la cual podemos trabajar. ¿Por qué? Porque usando este enfoque podemos agregar funcionalidad a un sistema sin romper la funcionalidad existente y sin causar cambios secundarios. Si podemos recomponer cosas de diferentes maneras para lograr diferentes resultados sin reescribir las piezas que estamos usando para componer nuestra solución, entonces hemos logrado un nivel de flexibilidad muy poderoso.

Cuando los módulos están alineados con el Principio Abierto/Cerrado, exhiben dos atributos como se menciona en la definición del principio. Son:

  • Abiertos para extensión. Esto significa que el módulo puede ser extendido de alguna manera para cambiar su comportamiento.
  • Cerrados para modificación. Si bien el módulo puede ser extendido para cambiar su comportamiento, no es necesario cambiar su código fuente. Idealmente, podríamos extender el comportamiento del módulo sin siquiera necesitar volver a compilar el binario.

Este principio no es realmente posible de seguir en un lenguaje procedural porque depende de alguna forma de abstracciones y polimorfismo para lograrlo.

Un Ejemplo:

Lo clásico de este principio es una declaración de switch que cambia en una enumeración u otra variable. Esta declaración switch casi siempre puede ser reemplazada por una jerarquía de clases que utiliza polimorfismo para ejecutar el método correcto; esta es la que sigue el PACO porque se puede inyectar un nuevo comportamiento agregando otra clase con un nuevo método polimórfico.

Al igual que con nuestra advertencia con el SRP, no siempre es posible anticipar las formas en que una clase necesitará ser extendida. La idea es usar nuestra experiencia para anticipar las formas en que una clase/módulo probablemente necesitará ser extendida y asegurarnos de que esté abierta para la extensión en esa área. Una vez más, utilizando el ejemplo de un deserializador, es muy probable que queramos extender esto agregando nuevos tipos de fuentes para los bytes a deserializar, por lo que necesitamos asegurarnos de que esté abierto para la extensión en esa área proporcionando una manera de enchufar una estrategia.

  Más sobre la inteligencia artificial

El Principio de Sustitución de Liskov

“Los subtipos deben ser sustituibles por sus tipos base”.

Cualquiera que haya usado herencia probablemente haya violado este principio muchas veces sin saberlo. En varias ocasiones, he heredado de una clase base y proporcionado anulaciones que simplemente lanzan una NotImplementedException() o similar. Esta subclase está violando el Principio de Sustitución de Liskov porque este subtipo no es sustituible por el tipo base en todas las circunstancias. Sin embargo, todavía hay momentos en que esto es conveniente. Un buen ejemplo es la biblioteca ADO.NET: no todos los proveedores de bases de datos implementan todas las características definidas por ADO.NET, pero la mayoría de ellos implementan la mayoría de las características, y sería muy difícil separar cada característica en diferentes interfaces, por lo que tiene más sentido simplemente lanzar algunas NotImplementedExceptions() aquí o allá que tratar de asegurar que el LSP nunca se viole.

Una forma de asegurar que se siga el LSP es separar las interfaces; es decir, seguir el Principio de Segregación de la Interfaz. Entonces, sus subtipos solo necesitarán implementar las interfaces que se apliquen directamente.

El Principio de Segregación de la Interfaz

“Los clientes no deben estar obligados a depender de métodos que no utilizan”.

Este principio es esencialmente un reconocimiento de que las clases cohesivas son un ideal y no siempre son posibles. Hay momentos en los que necesitamos construir una clase que tenga un gran conjunto de métodos en su interfaz; por ejemplo, cuando construimos una fachada que actúa como el adaptador para varios sistemas de backend diferentes. Sin embargo, el Principio de Segregación de la Interfaz dice que esta interfaz grande debería dividirse en secciones dependiendo de las necesidades de los diferentes tipos de clientes que se conectarán a ella.

Es decir, si hay un conjunto de métodos que será utilizado por un tipo de cliente y un conjunto diferente de métodos utilizado por otro, entonces estos métodos deberán dividirse en dos interfaces diferentes para que cada tipo de cliente pueda depender de una interfaz abstracta específica que solo contenga los métodos que se necesiten, en lugar de depender de la interfaz grande en sí misma.

La segregación de interfaces proporciona una forma de desacoplar las clases cliente. Si dos clases cliente diferentes dependen de una API concreta, entonces los cambios realizados en la API en beneficio de una clase cliente afectarán a la otra clase cliente, incluso si esos cambios no se hicieron en los métodos que necesita esa clase cliente. Esto significa que las dos clases cliente están acopladas entre sí a través de su dependencia común en la API. Si esa API se divide en dos interfaces segregadas diferentes y cada clase cliente depende de una diferente, entonces los cambios realizados en las implementaciones concretas solo afectarán al cliente que utiliza la interfaz afectada. Esto significa que las dos clases cliente ya no están acopladas entre sí.

El Principio de Inversión de Dependencias

“A. Los módulos de alto nivel no deben depender de los módulos de bajo nivel. Ambos deben depender de abstracciones. B. Las abstracciones no deben depender de detalles. Los detalles deben depender de abstracciones”.

Yo argumentaría que este es el principio S.O.L.I.D más ampliamente implementado. La idea está incrustada en cada “contenedor” que existe y ahora está mucho más integrada en frameworks como ASP.NET MVC.

La idea en el Principio de Inversión de Dependencias es invertir las dependencias naturales que normalmente surgen al codificar; por ejemplo, si está construyendo una interfaz de usuario y necesita llamar a una capa de datos para acceder a una base de datos, normalmente simplemente instanciaría un objeto de la capa de datos y lo llamaría. Esto es lo que Robert C. Martin llama el método procedimental tradicional. Él argumenta que el método orientado a objetos bien factorizado, sin embargo, es que la interfaz de usuario dependa de una abstracción de la capa de datos y tenga la implementación concreta dada para que esta dependencia tradicional se invierta.

Beneficios:

  • Reducción del acoplamiento entre clases concretas, lo que significa que cada una de ellas puede cambiar de manera aislada.
  • Permitir nuevas implementaciones de la capa de datos sin afectar la interfaz de usuario (el principio abierto/cerrado).
  • Permitir la inserción de decoradores u otras clases incluso en tiempo de ejecución o dependiendo de la configuración: por ejemplo, podríamos crear una capa de datos de registro que use la implementación de la capa de datos original pero agregue registro a ella.
  • Dependiendo de abstracciones significa que nuestras clases son menos frágiles ya que las abstracciones cambian con menos frecuencia que los detalles de implementación.
  • Probar cada una de las clases puede hacerse de manera aislada; no necesitamos una capa de datos concreta para probar la interfaz de usuario.
5/5 - (1 voto)

Deja una respuesta