Principio de Inversión de Dependencias (SOLID 5ª parte)
Antonio Leiva

Si te resultó interesante el principio de segregación de interfaces, el último de los principios SOLID, el principio de inversión de dependencias seguramente sea el que más cambie tu forma de programar una vez empieces a aplicarlo.

Si quieres tenerlo más cómodo, puedes descargarte el contenido en formato PDF y leerlo donde quieras. Te he preparado esta guía de Principios SOLID para ti.

Principio de inversión de dependencias

Este principio es una técnica básica, y será el que más presente tengas en tu día a día si quieres hacer que tu código sea testable y mantenible.

Gracias al principio de inversión de dependencias, podemos hacer que el código que es el núcleo de nuestra aplicación no dependa de los detalles de implementación, como pueden ser el framework que utilices, la base de datos, cómo te conectes a tu servidor…

Todos estos aspectos se especificarán mediante interfaces, y el núcleo no tendrá que conocer cuál es la implementación real para funcionar.

La definición que se suele dar es:

A. Las clases de alto nivel no deberían depender de las clases de bajo nivel. Ambas deberían depender de las abstracciones.

B. Las abstracciones no deberían depender de los detalles. Los detalles deberían depender de las abstracciones.

Pero entiendo que sólo con esto no te quede muy claro de qué estamos hablando, así que voy a ir desgranando un poco el problema, cómo detectarlo y un ejemplo.

El problema

En la programación vista desde el modo tradicional, cuando un módulo depende de otro módulo, se crea una nueva instancia y la utiliza sin más complicaciones.

Esta forma de hacer las cosas, que a primera vista parece la más sencilla y natural, nos va a traer bastantes problemas posteriormente, entre ellos:

  • Las parte más genérica de nuestro código (lo que llamaríamos el dominio o lógica de negocio) dependerá por todas partes de detalles de implementación. Esto no es bueno, porque no podremos reutilizarlo, ya que estará acoplado al framework de turno que usemos, a la forma que tengamos de persistir los datos, etc. Si cambiamos algo de eso, tendremos que rehacer también la parte más importante de nuestro programa.
  • No quedan claras las dependencias: si las instancias se crean dentro del módulo que las usa, es mucho más difícil detectar de qué depende nuestro módulo y, por tanto, es más difícil predecir los efectos de un cambio en uno de esos módulos. También nos costará más tener claro si estamos violando algunos otros principios, como el de Responsabilidad Única.
  • Es muy complicado hacer tests: Si tu clase depende de otras y no tienes forma de sustituir el comportamiento de esas otras clases, no puedes testarla de forma aislada. Si algo en los tests falla, no tendrías forma de saber de un primer vistazo qué clase es la culpable.

¿Cómo detectar que estamos violando el Principio de inversión de dependencias?

Este es muy fácil: cualquier instanciación de clases complejas o módulos es una violación de este principio.

Además, si escribes tests te darás cuenta muy rápido, en cuanto no puedas probar esa clase con facilidad porque dependa del código de otra clase.

Te estarás preguntando entonces cómo vas a hacer para darle a tu módulo todo lo que necesita para trabajar. Tendrás que utilizar alguna de las alternativas que existen para suministrarle esas dependencias.

Aunque hay varias, las que más se suelen utilizar son mediante constructor y mediante setters (funciones que lo único que hacen es asignar un valor).

¿Y entonces auién se encarga de proveer las dependencias? Lo más habitual es utilizar un inyector de dependencias: un módulo que se encarga de instanciar los objetos que se necesiten y pasárselos a las nuevas instancias de otros objetos.

Se puede hacer una inyección muy sencilla a mano, o usar alguna de las muchas librerías que existen si necesitamos algo más complejo.

En cualquier caso esto se escapa un poco del objeto de este artículo.

Si quieres ver un caso particular y algo más sobre inyección, puedes leer este artículo sobre inyección de dependencias en Android con Hilt.

Ejemplo

Imaginemos que tenemos una cesta de la compra que lo que hace es almacenar la información y llamar al método de pago para que ejecute la operación. Nuestro código sería algo así:

class Shopping { ... }

class ShoppingBasket {
    fun buy(shopping: Shopping?) {
        val db = SqlDatabase()
        db.save(shopping)
        val creditCard = CreditCard()
        creditCard.pay(shopping)
    }
}

class SqlDatabase {
    fun save(shopping: Shopping?) {
        // Saves data in SQL database
    }
}

class CreditCard {
    fun pay(shopping: Shopping?) {
        // Performs payment using a credit card
    }
}

Aquí estamos incumpliendo todas las reglas que impusimos al principio. Una clase de más alto nivel, como es la cesta de la compra, está dependiendo de otras de bajo nivel, como cuál es el mecanismo para almacenar la información o para realizar el método de pago. Se encarga de crear instancias de esos objetos y después utilizarlas.

Piensa ahora qué pasa si quieres añadir métodos de pago, o enviar la información a un servidor en vez de guardarla en una base de datos local. No hay forma de hacer todo esto sin desmontar toda la lógica. ¿Cómo lo solucionamos?

Primer paso, dejar de depender de concreciones. Vamos a crear interfaces que definan el comportamiento que debe dar una clase para poder funcionar como mecanismo de persistencia o como método de pago:

interface Persistence {
    fun save(shopping: Shopping?)
}

class SqlDatabase : Persistence {
    override fun save(shopping: Shopping?) {
        // Saves data in SQL database
    }
}

interface PaymentMethod {
    fun pay(shopping: Shopping?)
}

class CreditCard : PaymentMethod {
    override fun pay(shopping: Shopping?) {
        // Performs payment using a credit card
    }
}

¿Ves la diferencia? Ahora ya no dependemos de la implementación particular que decidamos. Pero aún tenemos que seguir instanciándolo en ShoppingBasket.

Nuestro segundo paso es invertir las dependencias. Vamos a hacer que estos objetos se pasen por constructor:

class ShoppingBasket(
    private val persistence: Persistence,
    private val paymentMethod: PaymentMethod
) {
    fun buy(shopping: Shopping?) {
        persistence.save(shopping)
        paymentMethod.pay(shopping)
    }
}

¿Y si ahora queremos pagar por Paypal y guardarlo en servidor? Definimos las concreciones específicas para este caso, y se las pasamos por constructor a la cesta de la compra:

class Server : Persistence {
    override fun save(shopping: Shopping?) {
        // Saves data in a server
    }
}

class Paypal : PaymentMethod {
    override fun pay(shopping: Shopping?) {
        // Performs payment using Paypal account
    }
}

Ya hemos conseguido nuestro objetivo. Además, si ahora queremos testear ShoppingBasket, podemos crear Test Doubles para las dependencias, de forma que nos permita probar la clase de forma aislada.

Conclusión

Como ves, este mecanismo nos obliga a organizar nuestro código de una manera muy distinta a como estamos acostumbrados, y en contra de lo que la lógica dicta inicialmente, pero a la larga compensa por la flexibilidad que otorga a la arquitectura de nuestra aplicación.

Y con esto terminamos la serie de artículos sobre principios SOLID. Si te ha gustado y quieres repasarlo, puedes hacerlo descargándote la guía gratuita sobre estos principios.

Pero como te comentaba en el primer artículo sobre qué son los Principios SOLID, hay otra ley que, aunque no forma parte de los mismos, muchas veces se explica junta.

Esta es la Ley de Demeter. Puedes leer sobre la Ley de Demeter en el siguiente artículo.

¿Qué te ha parecido? ¿Qué otros temas te gustaría que tratara en el blog? Déjame tu opinión en los comentarios

Quizá también te interese…

Unidirectional Data Flow: Qué es y cómo funciona en Android

Unidirectional Data Flow: Qué es y cómo funciona en Android

Hay algunos conceptos que están empezando a resonar muy fuerte en Android, y Unidirectional Data Flow es uno de ellos. Como puede que sepas si estás apuntado a la newsletter, me he planteado escribir una serie de artículos sobre MVI (Model View Intent), y me he topado...

20 Comentarios

    • Antonio Leiva

      Gracías! Me alegro de que sirvan de ayudan.

      Responder
  1. fon

    Gracias por esta serie de artículos sobre los principios SOLID. Muy ilustrativos.
    Esperando más !

    Responder
  2. rubenm

    Muy buen artículo. Una forma muy sencilla de explicar algo muy importante.

    Responder
  3. dvaqueiro

    Antonio Leiva, felicidades por tu serie de artículos sobre SOLID. Como seguidor de Uncle Bob es la mejor referencia que he encontrado en castellano.
    Ahora con respecto a la inyección de dependencias se me plantean ciertas dudas de forma recurrente. Imaginemos una relación más estrecha entre objetos del tipo agregación. Como por ejemplo un Carrito que contiene Productos. En este caso el carrito podría tener una propiedad para contener los productos que contiene en cada momento.
    La cuestión es, cabría usar inyección de dependencias en estos casos? Si fuese afirmativo, como se podría concretar un ejemplo?
    Muchas gracias de antemano.

    Responder
    • Antonio Leiva

      Muchas gracias! En principio aquí no haría falta inyección, tendrías un carrito con un método “añadirProducto”, y poco más. Otro tema es dónde se crean esos productos. Seguramente necesitarías un componente que los cree, probablemente a partir de una base de datos o una petición a red, y este sí que necesitaría estar inyectado en el lugar donde lo utilices. No sé si esto responde a tu pregunta.

      Responder
      • dvaqueiro

        No quiero ser insistente pero quizás no he enfocado mi duda con acierto. Retomando un poco el ejemplo inicial, supongamos que un usuario regresa a nuestra aplicación y por tanto queremos recuperar un Carrito que previamente había empezado, es decir, este carrito ahora se encuentra guardado en cualquier medio persistente y necesita volver a recuperarse para estar a disposición del usuario. Entiendo que siguiendo la filosofía SOLID, crearíamos dos objetos CarritoRespository y ProductosRepository. Con el primero obentendríamos el carrito del usuario (algo como findByUser(id)) y con el segundo los productos del carrito correspondiente (findByCarrito(id)), una vez que tenemos esos datos podríamos hacer un Carrito->setProductos(productosDeCarrito), osea, realmente estamos inyectando los productos al carrito…
        No se si esta reflexión en alto me lleva a una correcta interpretación de la filosofía SOLID o realmente se me está escapando algo…

        Responder
        • Antonio Leiva

          Esa parte que comentas depende un poco más de la capa de persistencia. Hay sistemas de persistencia que te devuelven el carrito con los productos dentro sin necesidad de hacerlo tú a mano. Pero suponiendo que tengas dos tablas, y que manualmente tengas que buscar los elementos relacionados de una tabla con la otra y añadirlos cuando crees los objetos, yo lo veo bien, sí. Lo de llamar a ese setter inyección o no, la verdad es que es lo de menos. Es un objeto compuesto por otros, así que no veo otra forma de crearlo que no sea “inyectando” sus subobjetos.

          El término de inyección lo veo más claro cuando en vez de estar haciendo el “new” de un objeto (que normalmente es un módulo con funcionalidad propia) se lo pasamos por constructor o por setter. Imaginemos que en tu ejemplo el “CarritoRepository” utiliza “ProductosRepository” para recuperar los productos. En vez de hacer un “new” dentro de la clase, “CarritosRepository” tendrá un parámetro en el constructor por ejemplo que será “ProductosRepository”. También se puede hacer inyección mediante un setter, como bien has comentado.

          Responder
          • Sebastian

            Increible respuesta Antonio, de verdad, increible, me queda más claro que lo que busca este principio es dejar de depender de concreciones, es decir, dejar de utilizar el new y hacer uso de lo que necesitemos a través de interfaces que sean implementadas por las clases dónde sea que esté lo que necesitemos usar, es decir, aumentar la cohesión y no el acoplamiento. Entiendo que esto se puede hacer a través de el constructor o de setters, como mencionas, no sé si nunca acabé de entender del todo el funcionamiento del constructor, pero podrías explicar como funciona este proceso a través de este y también cómo sería un ejemplo a través de setters? Gracias por la pasciencia.

          • Antonio Leiva

            Simplemente, en vez de hacer el new dentro de la clase, se lo pasas por el constructor de la clase. Con setters sería lo mismo solo que inicialmente el campo está a nulo y con setCampo(valor) le modificas el valor.

  4. Felipe

    Esta muy bien la explicación y es fácil de entender pero creo que aparte de la solución por medio de código, sería más entendible si también se utilizará solución visual,es decir, por medio de diagramas UML, de clases

    Responder
  5. nonynownn

    Waoo Excelente.. Mu claro y conciso
    Muchas Gracias me sirvió demasiado..! lo de los 5 Principios
    aunque no tanto este de Inversión :/

    Responder
  6. Simón

    Muchas gracias! Es una excelente serie de artículos, muy concisos y sencillos, te pregunto: Has escrito algo de programación reactiva? Con cual de estos principios puede tener conflicto si se abusa de ella? Gracias.

    Responder
    • Antonio Leiva

      No, de programación reactiva no sé mucho, no me veo en posición de poder hablar de ello. Gracias por tus palabras!

      Responder
  7. Josue echeverri

    Me encanto tu post, acabas de cambiar mi punto de vista de la programación.

    Responder
  8. JamiDev

    Me gustaría que crearas un mini proyecto pasa a paso donde utilzaras cada uno de los principios S.O.L.I.D y se vea en la práctica la importancia, funcionalidad y ventajas de utilizarlos. Gracias.

    SALUDOS desde Axochiapan, Morelos, México.

    Responder
    • Antonio Leiva

      No creo que pueda hacer algo parecido en un tiempo próximo, lo siento.

      Responder
  9. Jose

    Una duda, en el punto “Las clases de alto nivel no deberían depender de las clases de bajo nivel…”

    ¿No sería al revés? “Las clases de bajo nivel no deberían depender de las de alto nivel”

    Muchas gracias por enseñarnos. Un saludo.

    Responder
    • Antonio Leiva

      Con clases de alto nivel se refiere a clases que no entran en detalles de implementación. Una clase de bajo nivel, por el contrario, es la que hace cosas más “cercanas a la máquina”, como leer ficheros, conectarse a un servidor, etc.

      Responder

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de Antonio Leiva Gordillo, con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Acepto la política de privacidad *