Saltar al contenido

Buenas prácticas de desarrollo de software: Integrando principios SOLID

La calidad del código es un pilar esencial que determina no solo la eficiencia y funcionalidad del producto final, sino también su capacidad para adaptarse y evolucionar ante los nuevos requisitos y desafíos tecnológicos. En este contexto, los principios SOLID emergen como un faro de buenas prácticas en el diseño y la programación orientada a objetos, proporcionando un marco teórico y práctico para construir aplicaciones más robustas, mantenibles y escalables. 

Introducidos por Robert C. Martin, conocido afectuosamente como “Tío Bob”, estos cinco principios fundamentales ofrecen a los desarrolladores herramientas para enfrentar algunos de los problemas más comunes en el desarrollo de software, tales como el acoplamiento rígido, la baja cohesión y la dificultad en el testeo. 

A lo largo de este artículo, exploraremos en detalle cada uno de estos principios, desentrañando su importancia y aplicabilidad a través de ejemplos concretos, y destacando cómo su integración puede transformar el proceso de desarrollo de software en una práctica más elegante y sustentable.

Single Responsibility Principle (SRP)

El Principio de Responsabilidad Única (SRP) es uno de los fundamentos de los principios SOLID y sostiene que cada clase en un sistema de software debe tener una única razón para cambiar. Esto significa que cada clase debe enfocarse en una sola funcionalidad o responsabilidad dentro del sistema, evitando así la sobrecarga de clases con múltiples comportamientos o responsabilidades.

La importancia del SRP radica en su capacidad para facilitar un diseño de software limpio y modular. Al asegurar que cada clase tenga una única responsabilidad, el código se vuelve más legible, mantenible y susceptible a pruebas unitarias efectivas. Además, este principio reduce la complejidad del sistema, facilitando el proceso de desarrollo y evitando efectos secundarios inesperados durante la modificación o extensión del código.

Así pues, los beneficios resultantes los podríamos resumir en los siguientes: mejora de la mantenibilidad, facilidad en el escrito de pruebas, mayor reusabilidad y menos acoplamiento puesto que se disminuyen las dependencias entre clases.

Implementación del SRP

Para adherir al SRP, es crucial realizar un análisis detallado de los requerimientos y funcionalidades del sistema para identificar y separar las diferentes responsabilidades. Cada clase debe diseñarse de manera que encapsule una sola funcionalidad o aspecto del sistema. Si se identifica que una clase está manejando más de una responsabilidad, se debe considerar una refactorización para dividir esa clase en clases más pequeñas, cada una enfocada en una única tarea o función.

Trampas comunes y anti-patrones

Como acabamos de comentar, a veces las clases se sobrecargan con responsabilidades adicionales. Por ejemplo, una clase Libro no solo podría contener propiedades y métodos relacionados con un libro, sino también lógica para guardar este libro en una base de datos. Esta mezcla de lógica de dominio y lógica de persistencia es un anti-patrón común que viola el SRP.

Para adherir al SRP, se puede separar la lógica de persistencia en otra clase, garantizando que cada clase tenga una sola razón para cambiar. Este enfoque no solo hace el código más mantenible sino también más escalable.

Ejemplo práctico

A continuación, comentaremos un ejemplo en Python aplicado a un sistema de gestión de empleados en una empresa. Inicialmente, tenemos una clase Empleado que gestiona tanto la información personal del empleado como las operaciones de cálculo de su salario.

principios solid 1

En este diseño, la clase Empleado tiene múltiples responsabilidades: mantener la información del empleado y manejar su salario, así como la persistencia de estos datos. Esto viola el SRP, ya que hay más de una razón para cambiar esta clase.

Para adherirnos al SRP, podemos dividir esta clase en dos: una que maneje exclusivamente la información personal y de salario del empleado, y otra que se ocupe de la persistencia de los datos del empleado.

solid principios solid 2

En este enfoque, Empleado se enfoca en las propiedades y comportamientos directamente relacionados con el empleado, mientras que GestorDePersistenciaEmpleado maneja todas las interacciones con la base de datos relacionadas con los empleados. Esta separación de responsabilidades hace que el código sea más modular, más fácil de mantener y de entender.

Open/Closed Principle (OCP)

El Principio Abierto/Cerrado (OCP) establece que las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para la extensión, pero cerradas para la modificación. Lo que esto significa es que un componente de software debe ser extensible sin necesidad de modificar el código fuente existente.

Teniendo esto en cuenta, el OCP permite que el software crezca y cambie con requisitos nuevos o cambiantes, minimizando el riesgo de introducir errores en el código ya probado y funcional. Esto reduce el costo y el esfuerzo asociados con la implementación de nuevas funcionalidades o con la adaptación a diferentes contextos o requisitos.

Implementación del OCP

La implementación del OCP a menudo se logra mediante el uso de abstracciones (como interfaces o clases abstractas) y la composición sobre la herencia. Esto permite que los comportamientos se extiendan modificando cómo se compone un objeto (a través de la inyección de dependencias, por ejemplo) en lugar de cambiar el comportamiento existente.

Ejemplo práctico

A continuación, comentaremos un ejemplo en Python aplicado a un sistema de procesamiento de pagos donde necesitas manejar diferentes tipos de pagos (tarjeta de crédito, PayPal, criptomonedas, etc.). Sin el OCP, podrías encontrarte añadiendo constantemente nuevas condiciones y métodos a una clase de procesamiento de pagos cada vez que necesites soportar un nuevo método de pago, lo que viola el principio de estar “cerrado para la modificación”.

Una solución que sigue el OCP podría ser definir una interfaz IPago con un método procesarPago().

Luego, para cada método de pago, creas una clase que implemente IPago (como PagoTarjetaCredito, PagoPayPal, PagoCriptomoneda) como se muestra a continuación:

principios solid 3

Este diseño permite añadir nuevos métodos de pago al sistema simplemente creando nuevas clases que implementen IPago, sin necesidad de modificar el código existente que procesa los pagos. Esto cumple con el OCP al ser cerrado para modificaciones pero abierto para extensiones.

Liskov Substitution Principle (LSP)

El Principio de Sustitución de Liskov (LSP), formulado por Barbara Liskov en 1987, es un concepto fundamental en el diseño orientado a objetos. Este principio afirma que si una clase S es un subtipo de una clase T, entonces los objetos de tipo T en un programa pueden ser reemplazados con objetos de tipo S (es decir, objetos de la subclase) sin alterar ninguna de las propiedades deseables del programa (corrección, tarea que realiza, etc.).

El LSP es crucial para el diseño de sistemas robustos y mantenibles, ya que promueve la interoperabilidad y la reusabilidad del código. Al adherirse al LSP, los desarrolladores pueden extender y modificar sistemas con confianza, sabiendo que los nuevos subtipos no introducirán errores o comportamientos inesperados.

Implementación del LSP

Para adherirse al LSP, es esencial asegurarse de que las subclases no cambien el comportamiento de las superclases de manera que pueda sorprender al usuario del código. Esto incluye respetar las invariantes de la superclase, cumplir con los contratos de los métodos (incluidos los requisitos de los parámetros y los valores de retorno), y no introducir nuevas excepciones que no se puedan manejar.

Ejemplo práctico

A continuación, mostramos cómo se podría introducir una solución basada en interfaces que distinga entre las aves que pueden volar y las que no utilizando Python:

Primero, definimos una interfaz general para Ave y otra para las aves que pueden volar, AveVoladora.
A continuación, implementamos clases específicas para diferentes tipos de aves, asegurándonos de que solo las aves que pueden volar implementen la interfaz AveVoladora como se observa a continuación:

principios solid 4

En este diseño, Pájaro es capaz de comer y volar, por lo que implementa tanto Ave como AveVoladora. Por otro lado, Pingüino, que puede comer pero no volar, solo implementa Ave. Esto cumple con el LSP, ya que podemos sustituir cualquier Ave por un Pájaro o un Pingüino sin alterar el comportamiento esperado respecto a la capacidad de comer. Sin embargo, solo podemos esperar que un AveVoladora vuele.

Este enfoque garantiza que nuestras clases se adhieran al LSP, ya que evita que las subclases (como Pinguino) se vean forzadas a implementar métodos (como volar) que no pueden utilizar, manteniendo la coherencia y evitando comportamientos incorrectos o inesperados.

Interface Segregation Principle (ISP)

El Principio de Segregación de Interfaces (ISP) aborda cómo se deben estructurar las interfaces en un software para promover un diseño limpio y modular. En esencia, el ISP sugiere que “los clientes no deben ser forzados a depender de interfaces que no utilizan”.

La aplicación del ISP es fundamental para evitar el “inflado” de interfaces, donde una interfaz se carga con demasiados métodos que no son relevantes para todos sus consumidores. Esto lleva a la creación de implementaciones dependientes de partes de la interfaz que no necesitan, lo que puede resultar en código difícil de mantener y entender, además de aumentar el riesgo de errores en tiempo de ejecución.

Implementación del ISP

Para adherirse al ISP, las interfaces deben ser específicas a los requerimientos de sus clientes, en lugar de ser genéricas. Esto significa dividir interfaces grandes y extensivas en conjuntos más pequeños y específicos que sean relevantes para sus respectivos clientes. Esto promueve un diseño más limpio, donde las implementaciones solo necesitan conocer y depender de las interfaces que realmente utilizan.

Ejemplo práctico

Imagina un sistema de gestión para una biblioteca que incluye operaciones como imprimir recibos de préstamo, enviar notificaciones por email y guardar registros de préstamos. Inicialmente, podrías tener una interfaz grande IBibliotecaServicios con métodos para cada una de estas operaciones. Sin embargo, no todos los clientes de esta interfaz necesitarán todas estas operaciones. Por ejemplo, una clase que solo maneja la impresión de recibos no debería necesitar implementar métodos para enviar emails o guardar registros.

Para aplicar el ISP al sistema de gestión de la biblioteca, dividiremos la interfaz grande IBibliotecaServicios en interfaces más pequeñas y especializadas, tales como IReciboServicio, INotificacionServicio, y IRegistroServicio definiéndolas según sus funcionalidades. Cada cliente entonces implementaría solo las interfaces relevantes para sus necesidades, promoviendo así un diseño más limpio y modular.

A continuación, implementamos clases específicas que solo necesitan implementar las interfaces relevantes para sus responsabilidades:

principios solid 5

En este diseño, cada servicio se enfoca en una sola responsabilidad y solo implementa la interfaz correspondiente a esa responsabilidad. Por ejemplo, ReciboServicio solo se ocupa de imprimir recibos y, por tanto, solo implementa IReciboServicio. Esto significa que si una parte del sistema solo necesita gestionar la impresión de recibos, puede depender únicamente de IReciboServicio, sin preocuparse por las otras funcionalidades.

Este enfoque no solo cumple con el ISP, sino que también facilita el mantenimiento del código y mejora su extensibilidad, ya que añadir nuevas funcionalidades o modificar las existentes tiene un impacto mínimo en las otras partes del sistema.

Dependency Inversion Principle (DIP)

El Principio de Inversión de Dependencias (DIP) se enfoca en la forma en que se estructuran las dependencias dentro de un sistema de software, promoviendo una estructura que facilita la mantenibilidad y la flexibilidad. El DIP establece que:

  • Los módulos de alto nivel no deben depender de los módulos de bajo nivel. Ambos deben depender de abstracciones.
  • Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones.

La aplicación del DIP es crucial para evitar el acoplamiento rígido entre los módulos de software, lo que permite un sistema más fácil de modificar, extender y probar. Al depender de abstracciones en lugar de implementaciones concretas, los módulos de alto nivel no están directamente ligados a los detalles de los módulos de bajo nivel, lo que promueve un diseño más modular y reutilizable.

Implementación DIP

Implementar el DIP a menudo implica el uso de interfaces o clases abstractas para definir las abstracciones que forman el contrato entre diferentes módulos de un sistema. Los módulos de alto nivel, que contienen lógica de negocio o políticas, definen sus dependencias en términos de estas interfaces, mientras que los módulos de bajo nivel, que implementan detalles específicos como operaciones de base de datos o comunicaciones de red, implementan estas interfaces.

Ejemplo práctico

Para este caso, pondremos como ejemplo una aplicación de comercio electrónico con un módulo de alto nivel que gestiona los pedidos de los clientes y un módulo de bajo nivel que maneja el acceso a la base de datos de productos.

Sin el DIP, el módulo de gestión de pedidos podría depender directamente de la implementación concreta del módulo de acceso a la base de datos, lo que dificultaría, por ejemplo, cambiar a una nueva base de datos o fuente de datos.

Aplicando el DIP, introduciríamos una abstracción (por ejemplo, una interfaz IRepositorioProducto) que el módulo de gestión de pedidos usaría para interactuar con los productos.

A continuación, implementamos el módulo de acceso a la base de datos que maneja los productos, RepositorioProducto, el cual implementa la interfaz IRepositorioProducto. Esto podría representar, por ejemplo, un acceso a una base de datos relacional:

principios solid 6

Ahora, creamos el módulo de gestión de pedidos, GestorPedidos, que depende de la abstracción IRepositorioProducto en lugar de una implementación concreta, siguiendo el DIP:

principios solid 6

Finalmente, en la parte de configuración de nuestra aplicación o en el punto de entrada, inyectamos la dependencia específica del RepositorioProducto en GestorPedidos:

principios solid 7

Este diseño permite cambiar fácilmente la implementación del repositorio de productos sin modificar el GestorPedidos, por ejemplo, si decides cambiar de una base de datos relacional a una NoSQL. Solo necesitas crear una nueva clase que implemente IRepositorioProducto para la nueva base de datos y cambiar la instancia pasada a GestorPedidos. Esto demuestra cómo el DIP facilita un diseño flexible y fácilmente mantenible.

Conclusión

La adopción de los principios SOLID representa una inversión significativa en la calidad y sostenibilidad del desarrollo de software. A través de la exploración y aplicación práctica de cada principio, los desarrolladores pueden alcanzar un mayor entendimiento y apreciación por un diseño de software que no solo cumple con los requisitos actuales de manera eficaz, sino que también facilita la adaptación y crecimiento futuro.

Como hemos visto a lo largo del artículo, la importancia de estos principios radica en su capacidad para guiar hacia un código más limpio, desacoplado y fácilmente extensible, preparando el terreno para sistemas que puedan evolucionar de manera fluida ante los cambiantes requisitos del negocio y avances tecnológicos.

Por tanto, integrar los principios SOLID en el proceso de desarrollo no es solo una práctica recomendada, sino una filosofía que promueve la excelencia en el diseño de software, asegurando que cada línea de código contribuya a la construcción de aplicaciones robustas, mantenibles y escalables, que son, en última instancia, el corazón de la innovación tecnológica.

¿Quieres seguir leyendo sobre programación? ¡No te pierdas estos recursos!

En Block&Capital, nos esforzamos por crear un entorno donde el crecimiento y el éxito sean accesibles para todos. Si estás listo para impulsar tu carrera profesional, te animamos a unirte a nosotros.