Principios SOLID en programación orientada a objetos
Nos centramos en unos de los conocimientos y recomendaciones más recurrentes sobre diseño del software: los Principios SOLID. Son una serie de prácticas que fueron reunidas bajo un mismo paraguas por Robert C. Martin (también conocido como Uncle Bob) y sobre las cuales han corrido ríos de tinta. Si te dedicas a la programación y quieres mejorar la calidad de tu código, no debes dejar de estudiarlos y aplicarlos en tus proyectos.
¿Qué son los Principios SOLID?
Los Principios SOLID son un conjunto de cinco directrices fundamentales para mejorar la calidad del código, en factores diversos como la claridad, extensibilidad, flexibilidad, lo que deriva en una mejor mantenibilidad de las aplicaciones.
A lo largo de los últimos años los principios SOLID han sido una de las colecciones de consejos de diseño de software más renombradas en la industria del software, especialmente en los equipos de desarrollo ágiles. El acrónimo SOLID corresponde con estas recomendaciones o principios. Todos ellos extremadamente conocidos desde hace décadas en el mundo de la programación:
- S: Principio de Responsabilidad Única.
- O: Principio Abierto / Cerrado.
- L: Principio de Sustitución de Liskov.
- I: Principio de Segregación de Interfaces.
- D: Principio de Inversión de Dependencia.
Desglose de los Principios SOLID
A continuación, vamos a abordar cada uno de estos principios por separado para explicar sus bases fundamentales, que nos permitan comprender su significado.
S – Principio de Responsabilidad Única (Single Responsibility Principle)
Este principio establece que «cada clase debe tener una única razón para cambiar«. En realidad el Tío Bob está hablando de «Cohesión», es decir, una clase debe tener una única responsabilidad y por tanto sus miembros como propiedades y métodos deben ocuparse de una responsabilidad muy acotada. Si una clase está haciendo múltiples tareas es poco cohesiva y por lo tanto, se vuelve más difícil de mantener y probar.
O – Principio Abierto / Cerrado (Open/Closed Principle)
Este principio establece que el código debe estar abierto para su extensión, pero cerrado a las modificaciones. Esto significa que debemos desarrollar las clases de modo que puedan extender de manera sencilla su funcionalidad, sin necesidad de modificarla. Este principio reduce el riesgo de introducir errores durante el mantenimiento del código.
L – Principio de Sustitución de Liskov (Liskov Substitution Principle)
El Principio de Sustitución de Liskov establece que las subclases deben poder sustituir las clases base sin que ello altere o lleve a errores del programa. Para entender esto, si una clase B (coche) es una subclase de A (vehículo), deberías poder usar objetos de la clase B (coche) en lugar de objetos de la clase A (vehículo) sin que ello te lleve a fallos o comportamientos indeseados.
I – Principio de Segregación de Interfaces (Interface Segregation Principle)
Este principio dice que los clientes no deben verse obligados a depender de interfaces que no utilizan. En lugar de ello es preferible tener interfaces segregadas, más específicas, que se adapten mejor a las necesidades de los clientes.
También se ve fácilmente con un ejemplo: Si tienes una interfaz Ave que define métodos como volar() y nadar() puede que funcione bien en muchos casos. Sin embargo, si pensamos que no todas las aves pueden volar y nadar, puede ser un problema porque a veces querrás hacer algo con una clase que implementa Ave pero que no vuela (pingüino). Es por ello que es mejor segregar interfaces en algo como AveVoladora y AveNadadora.
D – Principio de Inversión de Dependencia (Dependency Inversion Principle)
Este principio establece que los módulos de alto nivel no deben depender de los módulos de bajo nivel. En lugar de ello se debería depender de abstracciones, que declaran una funcionalidad sin implementarla, como interfaces o clases abstractas. Esto fomenta un diseño más flexible y desacoplado de implementaciones concretas, lo que hace que sea más fácil de extender, probar y alterar.
Por ejemplo, una clase modelo no debe depender de un repositorio o servicio específico que implemente una tecnología de base de datos. En lugar de ello debe depender de una interfaz que define un conjunto de operaciones. Así podremos enviarle servicios de distintos tipos, con tecnologías distintas (por ejemplo motores de bases de datos distintos) y el modelo debería seguir funcionando correctamente.
Aplicación Práctica de los Principios SOLID
Si hemos podido entender los conceptos teóricos anteriores debemos ser capaces de aplicarlos en el día a día. Quizás puede costar un poco al principio, pero como desarrolladores debemos estar alerta para encontrar los posibles momentos donde la aplicación de estos principios pueda aportar mejoras en la calidad del código.
Ejemplos en lenguajes de programación populares
La verdad es que necesitaríamos códigos complejos para conseguir representar bien los contextos de aplicación de SOLID y es algo que no depende mucho del lenguaje en particular con el que estás trabajando, sino que tiene más que ver con las decisiones de diseño, puntos de extensión y el reparto de las responsabilidades. No obstante vamos a ver algunas aplicaciones prácticas de SOLID con algunos de los lenguajes de programación más populares.
Aplicación en Java
Como lenguaje puramente orientado a objetos, Java es un fuerte candidato para la aplicación de los principios SOLID. Vamos a ver un ejemplo esquemático del Principio de Inversión de Dependencia utilizando interfaces:
public interface NotificationService { void send(String message); } public class EmailNotificationService implements NotificationService { @Override public void send(String message) { // Código para enviar un email } } public class UserController { private NotificationService notificationService; public UserController(NotificationService notificationService) { this.notificationService = notificationService; } public void notifyUser() { notificationService.send('Bienvenido a la inversión de dependencias'); } }
En el código anterior has podido ver que UserController depende de la abstracción NotificationService en lugar de una clase concreta que implementa un tipo de notificación en particular. Esto facilita el intercambio de implementaciones como notificaciones por email, sockets o SMS.
Aplicación en C#
Vamos a ver ahora un código en C# que nos serviría para ilustrar la segregación de interfaces:
public interface IPrinter { void Print(); } public interface IScanner { void Scan(); } public class MultifunctionDevice : IPrinter, IScanner { public void Print() { // Código de impresión } public void Scan() { // Código de escaneo } }
Gracias a las recomendaciones del Principio de Segregación de Interfaces tenemos dos interfaces más pequeñas y específicas para dos distintas funcionalidades, aunque sea una misma clase las que las implementa.
Aplicación en Python
Python es un lenguaje más dinámico y flexible, débilmente tipado y con paradigma orientado a procesos y objetos. Pese a ello también se pueden aplicar los principios SOLID. Por ejemplo podemos ver una sencilla aplicación del Principio de Responsabilidad Única, separando la lógica de diferentes responsabilidades:
class User: def __init__(self, name): self.name = name class UserRepository: def save(self, user): # Código para guardar el usuario en la base de datos pass class UserNotificationService: def send_welcome_email(self, user): # Código para enviar un email pass
Es un código muy esquemático pero se puede apreciar cómo cada clase tiene una responsabilidad clara y única.
Ventajas de seguir los principios SOLID
La implementación de los principios SOLID, así como otros principios de diseño de software, tiene un impacto muy positivo en los proyectos de aplicaciones. Vamos a ver ahora algunas de las ventajas clave.
Mejora de la mantenibilidad del código
La facilidad de mantenimiento de las aplicaciones es uno de los beneficios más importantes de los proyectos bien diseñados. Los principios SOLID en general facilitan la extensión y la flexibilidad del software, limitan la complejidad y mejoran su robustez. Todas esas características son las que hacen que los proyectos sean más fácilmente mantenibles.
Facilita la colaboración en equipos de desarrollo
Atender a los principios de diseño permite a los desarrolladores crear código de calidad, más modular, estructurado y desacoplado. Esto hace que varios desarrolladores puedan trabajar de manera simultánea sin interferir en las tareas de los demás.
Reducción de errores durante el desarrollo
Los principios SOLID también hacen que se consiga un código más robusto y fácil de probar. Por tanto, es potencialmente menos propenso a errores en las etapas de mantenimiento y extensión del software.
Casos en los que no son adecuados los principios SOLID
Los principios SOLID son unas guías básicas de diseño que se adaptan a la mayor parte de los proyectos de software. Sin embargo, no siempre es necesario seguirlos a rajatabla, pues existen también casos en los que su aplicación hace que las soluciones no sean necesariamente mejores.
Aplicaciones pequeñas y proyectos de bajo presupuesto
En aplicaciones pequeñas es muy probable que los beneficios de aplicación de SOLID no sean suficientes para justificar el uso. Por supuesto, hay principios como única responsabilidad que deben ser siempre tenidos en cuenta, pero quizás no exista la necesidad de implementar inversión de dependencias porque el software no está sujeto a cambios.
Por tanto, la aplicación de ciertos principios pueden acarrear un tiempo y esfuerzo excesivo en comparación con los beneficios que ofrecería, aumentando el presupuesto innecesariamente.
Situaciones con requisitos de tiempo rigurosos
Intentar seguir algunos principios SOLID puede ralentizar el proceso de desarrollo. Si estamos cortos de tiempo o queremos simplemente lanzar un producto mínimo viable es posible que no los necesitemos y sea más práctico retrasar la aplicación de ciertos principios a fases posteriores.
Sistemas Legacy o código heredado
Si tenemos un proyecto con una gran base de código heredado (legacy code) puede resultar complejo aplicar SOLID de manera sistemática y estricta, pues quizás sea necesario reescribir muchas partes del software que están funcionando sin problemas y no están sujetas a cambios.
En este caso es importante evaluar qué partes del sistema pueden beneficiarse de una mejora y aplicarla de manera gradual, cuando los beneficios que vayamos a obtener sean necesarios para la extensión o el mantenimiento del software.
Proyectos con alta volatilidad en requisitos
En general, los principios SOLID impactan en la producción de software más flexible. Sin embargo, si los requisitos del proyecto están en constante cambio pueden inducir a un tiempo de desarrollo mayor, que no se justifique si esas partes del software se tienen que reescribir porque hayan cambiado de manera radical. Por tanto, podría ser más conveniente aplicar SOLID cuando los requisitos se estabilicen.
Escenarios con alto rendimiento y optimización crítica
Generalmente, es preferible buscar mejoras en el mantenimiento de las aplicaciones antes que la optimización. Sin embargo, en situaciones donde el rendimiento es una prioridad extrema, la aplicación de los principios SOLID puede agregar capas de abstracción innecesarias que impacten negativamente en la velocidad de procesamiento del software.