Alexandre Freire

Blog sobre desarrollo en Swift, iOS y Xcode

Principio de Inversión de Dependencias – SOLID

Jun 8, 2020

El Principio de Inversión de Dependencias, también conocido como Dependency Inversion Principle o DIP es el último de los cinco Principios SOLID de la programación orientada a objetos.

La definición que dio originalmente Uncle Bob es:

  1. Los módulos de alto nivel no deberían depender de los módulos de bajo nivel. Ambos deberían depender de abstracciones.
  2. Las abstracciones no deberían depender de los detalles. Los detalles deben depender de abstracciones.

Dicho así, puede resultar complicado saber a qué se refiere este principio, por lo que voy a intentar definir cada pieza:

  • Módulos de alto nivel: se refieren a los objetos que definen el qué es y qué hace tu programa. Aquellos que contienen la lógica de negocio y cómo interactúa el software entre sí. Son los objetos más importantes del programa. Por ejemplo, en una aplicación de un banco, podría ser el objeto que se encarga de devolver la lista de cuentas bancarias.
  • Módulos de bajo nivel: son aquellos objetos que no están directamente relacionados con la lógica de negocio del programa. Por ejemplo, el mecanismo de persistencia (CoreData, Realm, MySQL, etc…) o el mecanismo de acceso a red (URLSession, Alamofire, AFNetworking, etc…). Son objetos menos importantes, de los cuales no depende la lógica de negocio.
  • Abstracciones: se refieren a Tipos de Datos que no son las implementaciones concretas, si no los que definen la interfaz pública. Serán, por tanto, protocolos (o interfaces) o clases abstractas (*en Swift no existen las clases abstractas como tal)
  • Detalles: son las implementaciones concretas, que contienen detalles de implementación tales como:
    • Qué mecanismo de persistencia se utiliza (CoreData o Realm) — Objetos CoreDataDatabase o RealmDatabase), ó
    • Qué servicio se utiliza para acceder a la red (Alamofire o URLSession) — Objetos AlamofireWebService, o URLSessionWebService)

Los problemas que pueden resultar de no cumplir con el Principio de Inversión de Dependencias son:

  • Añades Acoplamiento entre módulos de alto nivel hacia módulos de bajo nivel. O lo que es lo mismo, tu lógica de negocio dependerá de detalles de implementación. Si alguna vez quieres cambiar algún detalle de implementación, por ejemplo utilizar URLSession en lugar de Alamofire, tendrás que modificar también los objetos más importantes del programa.
  • Las dependencias entre objetos no quedan claras: a simple vista, no puedes saber fácilmente de qué otros objetos depende un módulo.
  • Tu programa no es testable: si tu módulo depende de un objeto que no puedes intercambiar por un mock en tus tests no puedes testarla de manera unitaria. Si el test falla, no sabrás realmente qué objeto es el culpable del error.

¿Qué significa «Inversión de Dependencias?

En este contexto, podemos entender la palabar «Inversión» por cambiabilidad.

Ser capaz de «invertir» una dependencia es lo mismo que ser capaz de «intercambiar» una implementación concreta (un AlamofireWebService, que utiliza Alamofire por debajo para acceder a la red) por otra implementación concreta cualquiera (un URLSessionWebService, que utiliza URLSession por debajo).

Si tú puedes intercambiar una implementación concreta por otra y tu programa sigue funcionando perfectamente, entonces estarás cumpliendo con el Dependency Inversion Principle.

¿Es lo mismo «Inversión de Dependencias» que «Inyección de Dependencias?

No.

Inyección de Dependencias vs Inversión de Dependencias

La Inyección de Dependencias es una técnica que se utiliza para poder invertir las dependencias, pero el uso de la inyección no hace que automáticamente se puedan invertir.

En este artículo te explico mediante un ejemplo la diferencia entre ambos, y cómo utilizar la Inyección de Dependencias no es suficiente para cumplir con el Principio de Inversión de Dependencias.

Sigue leyendo para más información.

Principio de Inversión de Dependencias con ejemplos:

Imagina que estamos creando una app para este blog. La app muestra en un UITableView la lista de artículos publicados. Los artículos se descargan de un API y un endpoint ficticio, cuya URL sería https://alexandrefreire.com/api/v1/posts.

Vamos a ver qué objetos necesitaríamos:

Objeto WebService:

Para poder descargar la lista de posts, creamos un WebService (o APIClient) sencillo, utilizando la librería de terceros Alamofire.

Creamos un nuevo fichero llamado AlamofireWebService con únicamente un método get(_: completion:) para simplificar el tutorial:

El tipo del parámetro completion es Result<Data, Error>. Por algún motivo, el tipo del Result no se ve si uso el formato código.

import Alamofire

final class AlamofireWebService {

    func get(_ urlString: String, completion: (Result) -> Void) {
        AF.request(urlString)
            .responseData { response in
                if let data = response.value {
                    completion(.success(data))
                } else if let error = response.error {
                    completion(.failure(error))
                } else {
                    // More error handling here
                    ...
                }
            }
    }    
}

A continuación, vamos a crear un PostRepository para devolver un array de artículos [Post].

Objeto PostRepository:

En esta sección vas a crear el objeto PostRepository con su método allPosts(completion:). Dentro de él, vas a utilizar el AlamofireWebService para acceder a los datos del API, y un JSONDecoder para transformar los datos en un array de artículos:

El tipo del parámetro completion es Result<[Post], Error>. Por algún motivo, el tipo del Result no se ve si uso el formato código.

final class PostRepository {
    
    func allPosts(completion: (Result<[Post], Error>) -> Void) {
        // Se crea la instancia de AlamofireWebService
        let webService = AlamofireWebService()

        // La utilizamos
        webService.get("https://alexandrefreire.com/api/v1/posts") { result in
            switch result {
            case .success(let data):
                do {
                    let postList = try JSONDecoder().decode([Post].self, from: data)
                    completion(.success(postList))
                } catch {
                    completion(.failure(error))
                }
                
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

Por último, utilizarás este repositorio en el PostListViewController para mostrar los datos en un UITableView, aunque este código no lo voy a mostrar aquí porque no es parte del tutorial.

¿Qué problema tiene el código anterior?

Aunque, a priori, este código pueda parecer correcto y muy legible, existen varios problemas:

  • Estás añadiendo acoplamiento entre el Repositorio y el Servicio Web, ya que el primero sabe que está utilizando Alomofire e incluso se encarga de su propia instanciación.
  • A simple vista, no quedan claras las dependencias del Repositorio. Para saber que éste depende de un Servicio Web para acceder a los datos, tendríamos que leer de arriba a abajo el código para darnos cuenta.
  • Este Repositorio no es testable, ya que instancia directamente el objeto AlamofireWebService que accede a la red, impidiendo utilizar un mock en los tests.

En resumen, una clase de alto nivel (el repositorio) depende de otra de bajo nivel (la implementación concreta del Servicio Web), lo que hace que violemos el Principio de Inversión de Dependencias.

Inversión de dependencias VS Inyección de Dependencias

A menudo, se confunde el término «Inyección de Dependencias» con el de «Inversión de Dependencias» y se cree que aplicando Inyección de Dependencias ya se cumple el Principio de Inversión de Dependencias.

Esto no es así y en esta sección te explicaré el porqué.

La Inyección de Dependencias es una técnica que ayuda a aplicar la Inversión, pero por sí sola no hace que se puedan invertir las dependencias.

En el ejemplo anterior, el PostRepository estaba acoplado al AlamofireWebService ya que era el encargado de crear la instancia. Vamos a cambiar esto, quitándole a PostRepository la responsabilidad de construcción del AlamofireWebService, inyectándolo a través de su método init en lugar de crearlo dentro del método get:

final class PostRepository {
    
    // Añadimos una propiedad de tipo AlamofireWebService y se la inyectamos por constructor
    private let webService: AlamofireWebService
    
    init(webService: AlamofireWebService) {
        self.webService = webService
    }
    
    func allPosts(completion: (Result<[Post], Error>) -> Void) {

        // Usamos la propiedad webService en lugar de instanciarla aquí
        webService.get("https://alexandrefreire.com/api/v1/posts") { result in
            switch result {
            case .success(let data):
                do {
                    let postList = try JSONDecoder().decode([Post].self, from: data)
                    completion(.success(postList))
                } catch {
                    completion(.failure(error))
                }
                
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

En este código estás utilizando correctamente la Inyección de Dependencias, sin embargo, sigues sin poder invertirlas por lo que todavía se está violando el Principio de Inversión de Dependencias.

A pesar de inyectar las dependencias, no puedes cambiar la implementación que utiliza Alamofire para acceder a la red por otra que utilice URLSessionsin tener que cambiar el código del Repositorio.

Además, éste sigue acoplado a los detalles de implementación del Web Service, ya que la dependencia es de tipo AlamofireWebService.

Como puedes observar, aplicar Inyección de Dependencias no es suficiente. Sin embargo, es un mecanismo que te permitirá, con un par de cambios, cumplir con el Principio de Inversion de Dependencias.

Aplicando la Inversión de Dependencias

Con el código del ejemplo anterior, el Repositorio sigue acoplado a los detalles de implementación del Web Service.

Si en el futuro quieres borrar la dependencia de Alamofire y utilizar el mecanismo nativo URLSession, tendrías que modificar el código en el Repositorio. Es decir, para cambiar un simple detalle de implementación (bajo nivel), tienes que modificar código de la lógica de negocio (alto nivel). Veámoslo con el siguiente ejemplo…

En primer lugar, tendrías que implementar el Web Service utilizando URLSession. Añade un nuevo fichero Swift llamado URLSessionWebService:

final class URLSessionWebService {
    
    let session: URLSession
    
    init(session: URLSession = .shared) {
        self.session = session
    }
    
    func get(_ urlString: String, completion: (Result) -> Void) {
        guard let url = URL(string: urlString) else {
            fatalError("The given urlString is not a valid URL")
        }
        
        let urlRequest = URLRequest(url: url)
        let dataTask = session.dataTask(with: urlRequest) { data, response, error in
            if let error = error {
                completion(.failure(error))
            } else {
                guard let httpResponse = response as? HTTPURLResponse else {
                    fatalError("Unsupported protocol")
                }
                
                if 200 ..< 300 ~= httpResponse.statusCode {
                    if let data = data {
                        completion(.success(data))
                    }
                } else {
                    // More error handling here
                    ...
                }
            }
        }
        dataTask.resume()
    }
}

Una vez creado, tendrías que modificar el Repositorio inyectándole URLSessionWebService en lugar de AlamofireWebService:

final class PostRepository {
    
    // Utilizamos la implementación que usa URLSession 
    // como mecanismo de acceso a la red
    private let webService: URLSessionWebService
    
    init(webService: URLSessionWebService) {
        self.webService = webService
    }

    func allPosts(completion: (Result<[Post], Error>) -> Void) { 
        ...
    }
}

El Principio de Inversión de Dependencias busca evitar que para modificar un detalle de implementación tal como el mecanismo de acceso a red, te veas obligado a modificar el código que define la lógica de negocio, tal como el repositorio de artículos del blog.

¿Qué tendríamos que hacer para poder Invertir las dependencias sin tener que modificar PostRepository? Recuerda, Inversión significa cambiabilidad: poder intercambiar diferentes implementaciones del WebService (utilizando Alamofire, URLSession, o cualquier otra...) sin que el código del Repositorio se vea afectado.

Añadiendo la capacidad de Inversión de Dependencias

Lo que tenemos que hacer es abstraer la interfaz pública del WebService en un protocolo o clase abstracta. Dado que en Swift las clases abstractas no existen per sé, vamos a utilizar un protocolo :

protocol WebService: class {
    func get(_ urlString: String, completion: (Result) -> Void)
}

A continuación, hacemos que las diferentes implementaciones se conformen a este protocolo:

final class AlamofireWebService: WebService { 
    ...
}
final class URLSessionWebService: WebService {
    ...
}

Por último, hacemos que el PostRepository dependa de la abstracción y no de los detalles de implementación del WebService:

final class PostRepository {
    
    // Hacemos que dependa de la abstracción y no 
    // de los detalles de implementación
    private let webService: WebService
    
    init(webService: WebService) {
        self.webService = webService
    }

    func allPosts(completion: (Result<[Post], Error>) -> Void) { 
        webService.get("https://alexandrefreire.com/api/v1/posts") { result in
            ...
        }
    }
}

De esta manera, a la hora de crear el PostRepository, podríamos inyectarle cualquiera de las implementaciones de WebService, ya sea la que utiliza Alamofire o la que usa URLSession.

De esta manera, estamos cumpliendo el Principio de Inversión de Dependecias: podemos intercambiar las dependencias perfectamente ya que:

  1. Las clases de alto nivel (PostRepository) no depende de las clases de bajo nivel (AlamofireWebService ó URLSessionWebService), si no que depende de la abstracción (protocolo WebService)
  2. Las abstracciones no dependen de los detalles, sino que los detalles dependen de las abstracciones. En nuestro caso, tanto AlamofireWebService como URLSessionWebService dependen de la abstracción WebService. El protocolo WebService no depende de los detalles de implementación, es decir, no depende de Alamofire ó de URLSession.

¿Cómo saber si estamos violando el Dependency Inversion Principle?

La violación de este principio resulta muy sencillo de detectar.

Siempre que instancies un objeto complejo en una otro objeto que lo necesita se estará violando el Dependency Inversion Principle. Es decir, cuando que uses el método init (o new en otros lenguajes de programación) párate a pensar un par de minuto si tiene sentido inyectarlo en lugar de instanciarlo directamente.

Si no lo tiene, mueve la responsabilidad de construcción fuera del objeto e inyéctalo en lugar de instanciarlo dentro. A la hora de inyectar las dependecias, fíjate si estás inyectando la abstracción o la implementación concreta. Para que las dependencias sean Invertibles (o cambiables/intercambiables) debes inyectar un objeto de tipo protocolo, y no la clase que lo implementa directamente.

Únete a la newsletter para no perderte ningún nuevo tutorial: recibe un email cuando se publiquen nuevos artículos o vídeos y no pierdas la oportunidad de seguir aprendiendo. 

Conclusión

El Principio de Inversión de Dependencias es uno de los más importantes si quieres que tu código sea testable, legible y mantenible.

En resumen, cumplir con el DIP hace que el código de la lógica de negocio de tu aplicación no dependa de los detalles de implementación, si no de abstracciones. Esto hará que en el futuro puedas cambiar los detalles de implementación sin tener que modificar el código de tu lógica de negocio.

Con este principio se da por finalizada la serie sobre los Principios SOLID. Puedes echarle un vistazo a todos aquí o ir directamente a uno en concreto:

  1. Principio de Responsabilidad Única
  2. Principio Abierto/Cerrado
  3. Principio de Sustitución de Liskov
  4. Principio de Segregación de Interfaces
  5. Principio de Inversión de Dependencias

Si te ha gustado la serie, comparte en tus redes sociales para ayudarme a llegar a más desarrolladores

👇👇👇

¿Me ayudas a compartir en redes sociales?
Share on Facebook
Facebook
Pin on Pinterest
Pinterest
Tweet about this on Twitter
Twitter
Share on LinkedIn
Linkedin
Buffer this page
Buffer
Share on Reddit
Reddit

0 comentarios

Enviar un comentario

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