Hamburger Icon
Repaso a los principios SOLID: (D) Principio de Inversión de Dependencia

Repaso a los principios SOLID: (D) Principio de Inversión de Dependencia

Sir Isaac Newton era un poco flipado con su filia por la alquimia. Era también un poco cabroncete, por todos los pobres falsificadores que mandó ahorcar mientras se hizo cargo de la Real Casa de la Moneda. Pero lo que también es innegable, es que fue uno de los científicos más influyentes de la historia. Entre muchas otras cosas, formuló las Leyes del Movimiento Universal, que revolucionaron nuestra comprensión de cómo interactúan los objetos en el universo, incluidos los objetos en la POO.

Los principios SOLID son un conjunto de cinco principios de diseño de software que fueron propuestos por el programador Robert (tío Bob) Martin. Estos principios son guías que ayudan a desarrollar software que sea más mantenible, flexible y fácil de entender. Cada letra de la palabra "SOLID" representa uno de estos principios, en este post vamos a ver en qué consiste la D.

Antes de la D viene la I, por si te la saltaste.

DIP (Dependence Inversion Principle)

El Principio de Inversión de Dependencia se centra en la gestión de las dependencias entre los componentes de un sistema de software. Este principio fue propuesto por primera vez por Bertrand Meyer y es fundamental para lograr un diseño de software flexible y mantenible.

Definición

El Principio de Inversión de Dependencia establece dos conceptos clave:

1) Módulos de alto nivel no deben depender de módulos de bajo nivel: los módulos que contienen la lógica principal de la aplicación -alto nivel- no deben depender directamente de los detalles de implementación como bibliotecas -bajo nivel-. Los dos niveles deben depender de abstracciones.

Veámoslo con un ejemplo. Imagina que estás desarrollando un sistema de gestión de equipos de fútbol.

Tienes un módulo de alto nivel que se encarga de la lógica de negocio relacionada con los equipos y partidos, como la programación de partidos, la clasificación de equipos y la generación de informes.

Por otro lado, tienes un módulo de bajo nivel que contiene detalles de implementación, como la forma en que se accede y almacena la información de los equipos y partidos en una base de datos.

El problema surge si el módulo de alto nivel depende directamente del módulo de bajo nivel. Por ejemplo, supongamos que en la lógica de aplicación tienes una función para obtener la lista de jugadores de un equipo:

class TeamManager:
    def getPlayers(self, team_id):
        # Lógica para obtener los jugadores directamente desde la base de datos
        db = Database()
        players = db.query("SELECT player_name FROM players WHERE team_id = ?", team_id)
        return players

En este ejemplo, el módulo de alto nivel TeamManager depende directamente del módulo de bajo nivel Database. Esto crea un acoplamiento fuerte entre los dos módulos.

¿Por qué es un problema plantearlo así? Si en el futuro decides cambiar la forma en que se almacenan y obtienen los datos de los jugadores -por ejemplo, mediante webservice o un sistema de archivos- tendrás que modificar el módulo de alto nivel, lo que puede ser costoso y propenso a errores.

¿Cómo lo arreglamos? Introduciendo una abstracción entre los dos módulos:

class PlayersDataProvider:
    def getPlayers(self, team_id):
        pass

class DatabasePlayersDataProvider(PlayersDataProvider):
    def getPlayers(self, team_id):
        # Lógica para obtener los jugadores de la base de datos
        db = Database()
        players = db.query("SELECT player_name FROM players WHERE team_id = ?", team_id)
        return players

class TeamManager:
    def __init__(self, data_provider):
        self.data_provider = data_provider

    def getPlayers(self, team_id):
        # Utilizar el proveedor de datos para obtener los jugadores
        players = self.data_provider.getPlayers(team_id)
        return players

Ahora, el módulo de alto nivel TeamManager depende de la abstracción PlayersDataProvider, y no del módulo de bajo nivel directamente.

Puedes proporcionar diferentes implementaciones de PlayersDataProvider según sea necesario, como DatabasePlayersDataProvider, WebServicePlayersDataProvider o FilePlayersDataProvider, sin afectar la lógica en el módulo de alto nivel.

Esto hace que tu sistema sea más flexible y fácil de mantener, siguiendo el DIP y evitando la dependencia directa entre módulos de alto y bajo nivel.

¿Recuerdas lo que decía la Primera Ley de Newton o ley de la inercia?

Un objeto en reposo tiende a permanecer en reposo y un objeto en movimiento tiende a permanecer en movimiento a menos que actúe una fuerza externa. Podríamos relacionar esto con el DIP diciendo que "un sistema con dependencias rígidas tiende a permanecer rígido a menos que se aplique una inversión de dependencia".

Es decir, un sistema con dependencias fuertes tiende a ser difícil de cambiar a menos que se aplique el DIP para invertir esas dependencias y hacer que el sistema sea más flexible.

Veamos el otro concepto clave del principio.

2) Abstracciones no deben depender de Detalles: las abstracciones, que son interfaces o clases abstractas, no deben depender de detalles concretos de implementación.

Vamos a ver otro ejemplo. Imagina que estás desarrollando un sistema de gestión de dispositivos electrónicos, y uno de los componentes es un control remoto. Tienes una clase de control remoto llamada RemoteControl que debe ser capaz de controlar varios dispositivos, como televisores y reproductores de música:

class RemoteControl:
    def __init__(self, device):
        self.device = device

    def turn_on(self):
        self.device.turn_on()

    def turn_off(self):
        self.device.turn_off()

class TV:
    def turn_on(self):
        print("TV encendida")

    def turn_off(self):
        print("TV apagada")

class MusicPlayer:
    def turn_on(self):
        print("Reproductor de música encendido")

    def turn_off(self):
        print("Reproductor de música apagado")

# Uso del control remoto con una TV
tv_remote = RemoteControl(TV())
tv_remote.turn_on()  # Esto encenderá la TV

Aquí se ha violado el DIP ya que la clase RemoteControl depende directamente de las implementaciones concretas TV y MusicPlayer. Si quisieras agregar un nuevo tipo de dispositivo, como una radio, tendrías que modificar la clase RemoteControl, lo que no es ideal.

¿Lo arreglamos? Venga va.

from abc import ABC, abstractmethod

class Device(ABC):
    @abstractmethod
    def turn_on(self):
        pass

    @abstractmethod
    def turn_off(self):
        pass

class RemoteControl:
    def __init__(self, device):
        self.device = device

    def turn_on(self):
        self.device.turn_on()

    def turn_off(self):
        self.device.turn_off()

class TV(Device):
    def turn_on(self):
        print("TV encendida")

    def turn_off(self):
        print("TV apagada")

class MusicPlayer(Device):
    def turn_on(self):
        print("Reproductor de música encendido")

    def turn_off(self):
        print("Reproductor de música apagado")

# Uso del control remoto con una TV
tv_remote = RemoteControl(TV())
tv_remote.turn_on()  # Esto encenderá la TV

Hemos introducido la abstracción Device, que es una clase abstracta que define un contrato que deben cumplir todos los dispositivos.

La clase RemoteControl depende de esta abstracción en lugar de detalles concretos. Esto permite agregar nuevos dispositivos sin modificar RemoteControl, cumpliendo el DIP y evitando que la abstracción dependa de los detalles de implementación.

La Segunda Ley de Newton, F = ma (fuerza igual a masa por aceleración), sugiere que la fuerza aplicada a un objeto es directamente proporcional a su masa y su aceleración.

En el contexto del DIP, podríamos decir que "la dependencia es directamente proporcional a la masa del componente y su impacto en el sistema". Cuanto más fuerte sea la dependencia entre dos componentes, más difícil será cambiar uno de ellos sin afectar al otro.

Al aplicar el DIP, se reduce esta dependencia, permitiendo una mayor flexibilidad y facilidad de cambio.

Beneficios

Estos creo que son los beneficios más significativos que conlleva aplicar el Principio de Inversión de Dependencia en el diseño de software:

Flexibilidad en la implementación: hace que tu código sea más flexible en términos de implementación. Al depender de abstracciones en lugar de detalles concretos, puedes cambiar las implementaciones subyacentes sin afectar a las partes de alto nivel de tu sistema. Esto facilita la adaptación a cambios en los requisitos o la introducción de nuevas funcionalidades sin demasiados dolores de cabeza.

Mantenibilidad: reduce el acoplamiento entre los componentes de tu sistema, lo que facilita el mantenimiento. Puedes hacer cambios en una implementación sin tener que modificar muchas otras partes del código.

Reutilización de código: el DIP promueve la reutilización de código al hacer esta separación. Puedes utilizar las mismas abstracciones en múltiples partes de tu sistema con diferentes implementaciones según sea necesario.

Pruebas unitarias facilitadas: al depender de abstracciones, es más fácil escribir pruebas unitarias y realizar pruebas de integración. Puedes crear implementaciones de prueba para las abstracciones y sustituirlas en lugar de las implementaciones en producción para probar componentes de forma aislada.

Mejora en la colaboración: el DIP fomenta una mejor colaboración entre los equipos de desarrollo al permitir una mayor paralelización de tareas de programación.

Escalabilidad: puedes agregar fácilmente nuevas implementaciones de abstracciones existentes para adaptarte a nuevos requisitos o para escalar tu sistema a medida que crece.

Aplicación

Piensa en alguno de estos puntos para intentar aplicar este principio:

Identifica las dependencias. Identifica las dependencias entre los diferentes componentes de tu sistema. Esto incluye identificar las clases de alto nivel que dependen de las clases de bajo nivel, así como las clases concretas que se utilizan en lugar de abstracciones.

Definición e implementación de abstracciones. Crea abstracciones -interfaces o clases abstractas- que representen los contratos de las funcionalidades o servicios que necesitas en tu sistema.

Asegúrate de que estas abstracciones sean lo suficientemente generales para ser utilizadas por diferentes implementaciones. Documéntalas de manera clara para que los desarrolladores que trabajen en el sistema comprendan cómo deben interactuar con ellas.

Luego, crea implementaciones concretas que cumplan con los contratos definidos.

Inversión de control. Utiliza patrones de inversión de control, como Inyección de Dependencias o Service Locator, para inyectar las dependencias en lugar de que las clases de alto nivel las instancien directamente. Esto permite que las clases de alto nivel dependan de abstracciones y no de detalles concretos.

Conclusiones

Me viene genial terminar el post con La Tercera Ley de Newton, que establece que "a toda acción hay siempre una reacción igual y opuesta". Esto significa que cualquier fuerza aplicada a un objeto tiene un efecto en otro objeto.

En el contexto del DIP, podríamos decir que a toda dependencia hay una dependencia inversa igual y opuesta. Cuando una clase de alto nivel depende de una clase de bajo nivel, hay una dependencia que puede volverse rígida y difícil de cambiar. Sin embargo, al aplicar el DIP, se invierte esa dependencia, permitiendo una mayor flexibilidad y adaptación en el sistema.

En resumen, la aplicación del Principio de Inversión de Dependencia tiene múltiples beneficios, incluida la flexibilidad, la mantenibilidad, la reutilización de código y la mejora en la calidad del software en general. Ayuda a crear sistemas que son más adaptables a cambios y más fáciles de mantener a lo largo del tiempo.

Recuerda que aplicar el DIP puede requerir un cambio en la mentalidad de diseño y puede llevar tiempo. Sin embargo, los beneficios en términos de flexibilidad y mantenibilidad del código suelen justificar el esfuerzo invertido.

Quizá Newton era un flipado con su alquimia. Quizá yo sea un flipado por comparar el DIP con las Leyes del Movimiento Universal. De lo que no cabe duda es que, al igual que las fuerzas en el universo pueden ser influenciadas y equilibradas, las dependencias en el diseño de software pueden ser gestionadas y equilibradas mediante la inversión de dependencia.