Buscar..


Inyección de dependencia con controladores de vista

Introducción a la inyección dependiente

Una aplicación se compone de muchos objetos que colaboran entre sí. Los objetos suelen depender de otros objetos para realizar alguna tarea. Cuando un objeto es responsable de hacer referencia a sus propias dependencias, conduce a un código altamente acoplado, difícil de probar y difícil de cambiar.

La inyección de dependencia es un patrón de diseño de software que implementa la inversión de control para resolver dependencias. Una inyección es el paso de la dependencia a un objeto dependiente que la usaría. Esto permite una separación de las dependencias del cliente del comportamiento del cliente, lo que permite que la aplicación se acople de forma flexible.

No debe confundirse con la definición anterior: una inyección de dependencia simplemente significa darle a un objeto sus variables de instancia.

Es así de simple, pero proporciona muchos beneficios:

  • Más fácil probar su código (utilizando pruebas automatizadas como pruebas de unidad y UI)
  • cuando se usa en tándem con programación orientada a protocolos, facilita la modificación de la implementación de una clase determinada, más fácil de refactorizar
  • Hace que el código sea más modular y reutilizable.

Hay tres formas más comunes de implementar la inyección de dependencia (DI) en una aplicación:

  1. Inyección de inicializador
  2. Inyección de propiedad
  3. Uso de marcos DI de terceros (como Swinject, Cleanse, Dip o Typhoon)

Hay un artículo interesante con enlaces a más artículos acerca de la inyección de dependencia, así que verifique si desea profundizar en el principio de DI e Inversión de control.

Vamos a mostrar cómo usar DI con controladores de vista: una tarea diaria para un desarrollador de iOS promedio.

Ejemplo sin DI

Tendremos dos controladores de vista: LoginViewController y TimelineViewController . LoginViewController se utiliza para iniciar sesión y, una vez que se ha iniciado correctamente, cambiará al TimelineViewController. Ambos controladores de vista dependen de FirebaseNetworkService .

LoginViewController

class LoginViewController: UIViewController {

    var networkService = FirebaseNetworkService()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

TimelineViewController

class TimelineViewController: UIViewController {

    var networkService = FirebaseNetworkService()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func logoutButtonPressed(_ sender: UIButton) {
        networkService.logutCurrentUser()
    }
}

FirebaseNetworkService

class FirebaseNetworkService {

    func loginUser(username: String, passwordHash: String) {
        // Implementation not important for this example
    }
    
    func logutCurrentUser() {
        // Implementation not important for this example
    }
}

Este ejemplo es muy simple, pero supongamos que tiene 10 o 15 controladores de vista diferentes y algunos de ellos también dependen de FirebaseNetworkService. En algún momento, usted desea cambiar Firebase como su servicio backend con el servicio interno de su compañía. Para hacer eso, tendrá que pasar por cada controlador de vista y cambiar FirebaseNetworkService con CompanyNetworkService. Y si algunos de los métodos en CompanyNetworkService han cambiado, tendrá mucho trabajo por hacer.

Las pruebas de unidad y UI no son el alcance de este ejemplo, pero si quisiera probar en unidad los controladores de vista con dependencias estrechamente acopladas, sería muy difícil hacerlo.

Reescribamos este ejemplo e inyectemos el servicio de red a nuestros controladores de visualización.

Ejemplo con inyección de dependencia

Para aprovechar al máximo la inyección de dependencia, definamos la funcionalidad del servicio de red en un protocolo. De esta manera, los controladores de vista que dependen de un servicio de red ni siquiera tendrán que conocer la implementación real de este.

protocol NetworkService {
    func loginUser(username: String, passwordHash: String)
    func logutCurrentUser()
}

Agregue una implementación del protocolo NetworkService:

class FirebaseNetworkServiceImpl: NetworkService {
    func loginUser(username: String, passwordHash: String) {
        // Firebase implementation
    }
    
    func logutCurrentUser() {
        // Firebase implementation
    }
}

Cambiemos LoginViewController y TimelineViewController para utilizar el nuevo protocolo NetworkService en lugar de FirebaseNetworkService.

LoginViewController

class LoginViewController: UIViewController {

    // No need to initialize it here since an implementation
    // of the NetworkService protocol will be injected
    var networkService: NetworkService?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

TimelineViewController

class TimelineViewController: UIViewController {

    var networkService: NetworkService?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func logoutButtonPressed(_ sender: UIButton) {
        networkService?.logutCurrentUser()
    }
}

Ahora, la pregunta es: ¿Cómo inyectamos la implementación correcta de NetworkService en LoginViewController y TimelineViewController?

Dado que LoginViewController es el controlador de vista de inicio y se mostrará cada vez que se inicie la aplicación, podemos inyectar todas las dependencias en AppDelegate .

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // This logic will be different based on your project's structure or whether
    // you have a navigation controller or tab bar controller for your starting view controller
    if let loginVC = window?.rootViewController as? LoginViewController {
        loginVC.networkService = FirebaseNetworkServiceImpl()
    }
    return true
}

En el AppDelegate simplemente estamos tomando la referencia al primer controlador de vista (LoginViewController) e inyectando la implementación de NetworkService usando el método de inyección de propiedad.

Ahora, la siguiente tarea es inyectar la implementación de NetworkService en el TimelineViewController. La forma más fácil es hacerlo cuando LoginViewController está en transición a TimlineViewController.

Agregaremos el código de inyección en el método prepareForSegue en el LoginViewController (si está utilizando un método diferente para navegar a través de los controladores de vista, coloque el código de inyección allí).

Nuestra clase LoginViewController se ve así ahora:

class LoginViewController: UIViewController {
    // No need to initialize it here since an implementation
    // of the NetworkService protocol will be injected
    var networkService: NetworkService?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "TimelineViewController" {
            if let timelineVC = segue.destination as? TimelineViewController {
                // Injecting the NetworkService implementation
                timelineVC.networkService = networkService
            }
        }
    }
}

Hemos terminado y es así de fácil.

Ahora imagine que queremos cambiar nuestra implementación de NetworkService de Firebase a la implementación backend personalizada de nuestra empresa. Todo lo que tendríamos que hacer es:

Agregar nueva clase de implementación de NetworkService:

class CompanyNetworkServiceImpl: NetworkService {
    func loginUser(username: String, passwordHash: String) {
        // Company API implementation
    }
    
    func logutCurrentUser() {
        // Company API implementation
    }
}

Cambie FirebaseNetworkServiceImpl con la nueva implementación en AppDelegate:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // This logic will be different based on your project's structure or whether
        // you have a navigation controller or tab bar controller for your starting view controller
        if let loginVC = window?.rootViewController as? LoginViewController {
            loginVC.networkService = CompanyNetworkServiceImpl()
        }
        return true
    }

Eso es todo, hemos cambiado toda la implementación subyacente del protocolo NetworkService sin siquiera tocar LoginViewController o TimelineViewController.

Como este es un ejemplo simple, es posible que no vea todos los beneficios en este momento, pero si intenta usar DI en sus proyectos, verá los beneficios y siempre usará la inyección de dependencia.

Tipos de inyección de dependencia

Este ejemplo mostrará cómo usar el patrón de diseño de inyección de dependencia ( DI ) en Swift usando estos métodos:

  1. Inyección de inicializador (el término correcto es Inyección de constructor, pero como Swift tiene inicializadores, se llama inyección de inicializador)
  2. Inyección de propiedad
  3. Método de inyección

Ejemplo de configuración sin DI

    protocol Engine {
        func startEngine()
        func stopEngine()
    }
    
    class TrainEngine: Engine {
        func startEngine() {
            print("Engine started")
        }
        
        func stopEngine() {
            print("Engine stopped")
        }
    }
    
    protocol TrainCar {
    var numberOfSeats: Int { get }
    func attachCar(attach: Bool)
}

class RestaurantCar: TrainCar {
    var numberOfSeats: Int {
        get {
            return 30
        }
    }
    func attachCar(attach: Bool) {
        print("Attach car")
    }
}

class PassengerCar: TrainCar {
    var numberOfSeats: Int {
        get {
            return 50
        }
    }
    func attachCar(attach: Bool) {
        print("Attach car")
    }
}
    
class Train {
    let engine: Engine?
    var mainCar: TrainCar?
}

Inyección de dependencia del inicializador

Como su nombre lo indica, todas las dependencias se inyectan a través del inicializador de clase. Para inyectar dependencias a través del inicializador, agregaremos el inicializador a la clase Train .

La clase de tren ahora se ve así:

class Train {
    let engine: Engine?
    var mainCar: TrainCar?
    
    init(engine: Engine) {
        self.engine = engine
    }
}

Cuando deseamos crear una instancia de la clase Train, usaremos el inicializador para inyectar una implementación de Motor específica:

let train = Train(engine: TrainEngine())

NOTA: La principal ventaja de la inyección de inicializador frente a la inyección de propiedades es que podemos establecer la variable como variable privada o incluso convertirla en una constante con la palabra clave let (como hicimos en nuestro ejemplo). De esta manera podemos asegurarnos de que nadie pueda acceder o cambiarlo.

Propiedades Dependencia Inyección

DI usando propiedades es incluso más simple que usar un inicializador. Inyectemos una dependencia de PassengerCar en el objeto de tren que ya creamos utilizando las propiedades DI:

train.mainCar = PassengerCar()

Eso es. mainCar nuestro tren ahora es una instancia de PassengerCar .

Método de inyección de dependencia

Este tipo de inyección de dependencia es un poco diferente a los dos anteriores porque no afectará a todo el objeto, pero solo inyectará una dependencia que se utilizará en el alcance de un método específico. Cuando una dependencia solo se usa en un solo método, generalmente no es bueno hacer que todo el objeto dependa de él. Agreguemos un nuevo método a la clase de tren:

func reparkCar(trainCar: TrainCar) {
    trainCar.attachCar(attach: true)
    engine?.startEngine()
    engine?.stopEngine()
    trainCar.attachCar(attach: false)
}

Ahora, si llamamos al nuevo método de clase de Train, inyectaremos el TrainCar utilizando el método de inyección de dependencia.

train.reparkCar(trainCar: RestaurantCar())


Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow