Recherche…


Injection de dépendance avec les contrôleurs de vue

Injection Dependenct Intro

Une application est composée de nombreux objets qui collaborent entre eux. Les objets dépendent généralement d'autres objets pour effectuer certaines tâches. Lorsqu'un objet est responsable de référencer ses propres dépendances, il en résulte un code hautement couplé, difficile à tester et difficile à changer.

L'injection de dépendance est un modèle de conception logicielle qui implémente l'inversion du contrôle pour résoudre les dépendances. Une injection consiste à transmettre une dépendance à un objet dépendant qui l'utilise. Cela permet de séparer les dépendances du client du comportement du client, ce qui permet à l'application d'être couplée de manière souple.

Ne pas confondre avec la définition ci-dessus - une injection de dépendance signifie simplement donner à un objet ses variables d'instance.

C'est aussi simple que cela, mais il offre de nombreux avantages:

  • plus facile de tester votre code (en utilisant des tests automatisés comme les tests d'unité et d'interface utilisateur)
  • lorsqu'il est utilisé en tandem avec une programmation orientée protocole, il est facile de modifier l'implémentation d'une certaine classe - plus facile à restructurer
  • cela rend le code plus modulaire et réutilisable

Il existe trois méthodes les plus couramment utilisées pour l’injection de dépendance (DI) dans une application:

  1. Injection d'initialisation
  2. Injection de propriété
  3. Utiliser des frameworks DI tiers (comme Swinject, Cleanse, Dip ou Typhoon)

Il y a un article intéressant avec des liens vers d'autres articles sur l'injection de dépendances, alors jetez-y un coup d'œil si vous voulez approfondir le principe du DI et de l'inversion du contrôle.

Montrons comment utiliser DI avec View Controllers - une tâche quotidienne pour un développeur iOS moyen.

Exemple sans DI

Nous aurons deux contrôleurs de vue: LoginViewController et TimelineViewController . LoginViewController est utilisé pour se connecter et en cas de succès, il basculera vers le TimelineViewController. Les deux contrôleurs de vue dépendent du 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
    }
}

Cet exemple est très simple, mais supposons que vous ayez 10 ou 15 contrôleurs de vue différents et que certains d'entre eux dépendent également du FirebaseNetworkService. À un moment donné, vous souhaitez modifier Firebase en tant que service backend avec le service back-end de votre entreprise. Pour ce faire, vous devez passer par chaque contrôleur de vue et modifier FirebaseNetworkService avec CompanyNetworkService. Et si certaines des méthodes de CompanyNetworkService ont changé, vous aurez beaucoup de travail à faire.

Les tests unitaires et d'interface utilisateur ne sont pas la portée de cet exemple, mais si vous vouliez unifier les contrôleurs de vue de test avec des dépendances étroitement couplées, vous auriez vraiment du mal à le faire.

Réécrivons cet exemple et injectons le service réseau dans nos contrôleurs de vue.

Exemple avec injection de dépendance

Pour tirer le meilleur parti de l'injection de dépendances, définissons les fonctionnalités du service réseau dans un protocole. De cette façon, les contrôleurs de vue dépendant d'un service réseau n'auront même pas besoin d'en connaître la mise en œuvre réelle.

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

Ajoutez une implémentation du protocole NetworkService:

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

Modifions LoginViewController et TimelineViewController pour utiliser le nouveau protocole NetworkService au lieu 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()
    }
}

Maintenant, la question est la suivante: comment pouvons-nous injecter l'implémentation NetworkService correcte dans LoginViewController et TimelineViewController?

Puisque LoginViewController est le contrôleur de vue de départ et qu'il s'affiche à chaque démarrage de l'application, nous pouvons injecter toutes les dépendances dans 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
}

Dans AppDelegate, nous prenons simplement la référence au premier contrôleur de vue (LoginViewController) et injectons l'implémentation NetworkService à l'aide de la méthode d'injection de propriété.

Maintenant, la tâche suivante consiste à injecter l'implémentation NetworkService dans le TimelineViewController. Le moyen le plus simple consiste à le faire lorsque LoginViewController est en transition vers TimlineViewController.

Nous allons ajouter le code d'injection dans la méthode prepareForSegue dans LoginViewController (si vous utilisez une approche différente pour naviguer dans les contrôleurs de vues, placez-y le code d'injection).

Notre classe LoginViewController ressemble à ceci maintenant:

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
            }
        }
    }
}

Nous avons terminé et c'est aussi simple que cela.

Maintenant, imaginons que nous souhaitons faire passer notre implémentation NetworkService de Firebase à l'implémentation backend de notre société personnalisée. Tout ce que nous aurions à faire est de:

Ajoutez une nouvelle classe d'implémentation NetworkService:

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

Basculez le FirebaseNetworkServiceImpl avec la nouvelle implémentation dans 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
    }

Ça y est, nous avons changé toute l'implémentation sous-jacente du protocole NetworkService sans même toucher à LoginViewController ou TimelineViewController.

Comme il s'agit d'un exemple simple, vous pourriez ne pas voir tous les avantages dès maintenant, mais si vous essayez d'utiliser DI dans vos projets, vous en verrez les avantages et vous utiliserez toujours l'injection de dépendance.

Types d'injection de dépendance

Cet exemple démontrera comment utiliser modèle de conception d'injection de dépendance (DI) à Swift en utilisant ces méthodes:

  1. Initializer Injection (le terme approprié est Constructor Injection, mais étant donné que les initialiseurs de Swift sont appelés des initialiseurs d'injection)
  2. Injection de propriété
  3. Méthode injection

Exemple d'installation sans 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?
}

Injection de dépendance d'initialisation

Comme son nom l'indique, toutes les dépendances sont injectées via l'initialiseur de classe. Pour injecter des dépendances via l'initialiseur, nous allons ajouter l'initialiseur à la classe Train .

La classe de train ressemble maintenant à ceci:

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

Lorsque nous voulons créer une instance de la classe Train, nous utiliserons l'initialiseur pour injecter une implémentation spécifique du moteur:

let train = Train(engine: TrainEngine())

REMARQUE: Le principal avantage de l'injection de l'initialiseur par rapport à l'injection de propriété est que nous pouvons définir la variable en tant que variable privée ou même en faire une constante avec le mot clé let (comme nous l'avons fait dans notre exemple). De cette façon, nous pouvons nous assurer que personne ne peut y accéder ou le modifier.

Propriétés Injection de dépendance

DI utilisant des propriétés est encore plus simple que l'utilisation d'un initialiseur. Nous allons injecter une dépendance PassengerCar à l’objet de train que nous avons déjà créé en utilisant les propriétés DI:

train.mainCar = PassengerCar()

C'est tout. mainCar notre train est maintenant une instance PassengerCar .

Méthode d'injection de dépendance

Ce type d'injection de dépendances est un peu différent des deux précédentes car il n'affectera pas l'objet entier, mais n'injectera qu'une dépendance à utiliser dans le cadre d'une méthode spécifique. Lorsqu'une dépendance n'est utilisée que dans une seule méthode, il n'est généralement pas bon de la rendre dépendante de l'objet entier. Ajoutons une nouvelle méthode à la classe Train:

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

Maintenant, si nous appelons la méthode de classe du nouveau Train, nous allons injecter le TrainCar utilisant l'injection de dépendance de méthode.

train.reparkCar(trainCar: RestaurantCar())


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow