Ricerca…


Iniezione delle dipendenze con View Controller

Iniezione dell'iniezione di Dependenct

Un'applicazione è composta da molti oggetti che collaborano tra loro. Gli oggetti di solito dipendono da altri oggetti per eseguire alcune attività. Quando un oggetto è responsabile del riferimento alle proprie dipendenze, conduce a un codice altamente accoppiato, difficile da testare e difficile da modificare.

L'iniezione di dipendenza è un modello di progettazione software che implementa l'inversione del controllo per la risoluzione delle dipendenze. Un'iniezione sta passando dipendenza a un oggetto dipendente che lo userebbe. Ciò consente una separazione delle dipendenze del client dal comportamento del client, che consente all'applicazione di essere accoppiata liberamente.

Da non confondere con la definizione di cui sopra - un'iniezione di dipendenza significa semplicemente dare a un oggetto le sue variabili di istanza.

È così semplice, ma offre molti vantaggi:

  • più facile testare il codice (utilizzando test automatici come test di unità e UI)
  • se utilizzato in tandem con la programmazione orientata ai protocolli, semplifica il cambio di implementazione di una determinata classe, più semplice da refactoring
  • rende il codice più modulare e riutilizzabile

Esistono tre modi più comuni in cui la Dependency Injection (DI) può essere implementata in un'applicazione:

  1. Iniezione inizializzatore
  2. Iniezione di proprietà
  3. Utilizzo di framework DI di terze parti (come Swinject, Cleanse, Dip o Typhoon)

C'è un articolo interessante con collegamenti ad altri articoli su Dipendenza Iniezione, quindi dai un'occhiata se vuoi approfondire i principi DI e Inversion of Control.

Mostriamo come usare DI con View Controller - un compito quotidiano per uno sviluppatore iOS medio.

Esempio senza DI

Avremo due View Controller: LoginViewController e TimelineViewController . LoginViewController viene utilizzato per accedere e, una volta completato lo schema, passerà a TimelineViewController. Entrambi i controller di visualizzazione dipendono da 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
    }
}

Questo esempio è molto semplice, ma supponiamo di avere 10 o 15 diversi controller di visualizzazione e alcuni di essi dipendono anche da FirebaseNetworkService. Ad un certo momento, desideri cambiare Firebase come servizio di back-end con il servizio di back-end della tua azienda. Per farlo dovrai passare attraverso ogni controller di visualizzazione e modificare FirebaseNetworkService con CompanyNetworkService. E se alcuni dei metodi in CompanyNetworkService sono cambiati, avrai molto lavoro da fare.

Il test dell'Unità e dell'UI non è lo scopo di questo esempio, ma se si volessero testare i controller delle viste di prova con dipendenze strettamente accoppiate, si avrebbe davvero molto tempo a farlo.

Riscriviamo questo esempio e inseriamo il servizio di rete nei nostri controller di visualizzazione.

Esempio con iniezione di dipendenza

Per sfruttare al meglio l'iniezione delle dipendenze, definiamo la funzionalità del servizio di rete in un protocollo. In questo modo, i controller di visualizzazione dipendenti da un servizio di rete non dovranno nemmeno sapere della reale implementazione di esso.

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

Aggiungi un'implementazione del protocollo NetworkService:

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

Cambiamo LoginViewController e TimelineViewController per utilizzare il nuovo protocollo NetworkService invece di 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()
    }
}

Ora, la domanda è: come iniettare l'implementazione corretta di NetworkService in LoginViewController e TimelineViewController?

Poiché LoginViewController è il controller di visualizzazione iniziale e mostrerà ogni volta che l'applicazione si avvia, è possibile iniettare tutte le dipendenze in 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
}

In AppDelegate stiamo semplicemente prendendo il riferimento al primo controller di visualizzazione (LoginViewController) e iniettando l'implementazione NetworkService usando il metodo di iniezione di proprietà.

Ora, il prossimo compito è quello di iniettare l'implementazione NetworkService in TimelineViewController. Il modo più semplice è farlo quando LoginViewController sta passando a TimlineViewController.

Aggiungeremo il codice di iniezione nel metodo prepareForSegue nel LoginViewController (se si sta utilizzando un approccio diverso per navigare tra i controller di vista, inserire lì il codice di iniezione).

La nostra classe LoginViewController si presenta ora come questa:

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

Abbiamo finito ed è così facile.

Ora immagina di voler passare dall'implementazione di NetworkService da Firebase all'implementazione del backend della nostra azienda personalizzata. Tutto ciò che dovremmo fare è:

Aggiungi una nuova classe di implementazione NetworkService:

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

Passa a FirebaseNetworkServiceImpl con la nuova implementazione in 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
    }

È così, abbiamo cambiato l'intera implementazione underlay del protocollo NetworkService senza nemmeno toccare LoginViewController o TimelineViewController.

Dato che questo è un semplice esempio, potresti non vedere tutti i benefici al momento, ma se provi a usare DI nei tuoi progetti, vedrai i benefici e userai sempre Dependency Injection.

Tipi di iniezione delle dipendenze

Questo esempio mostrerà come utilizzare il pattern di progettazione Dipendenza Iniezione ( DI ) in Swift usando questi metodi:

  1. Iniezione inizializzatore (il termine corretto è Iniezione costruttore, ma poiché Swift ha inizializzatori si chiama iniezione inizializzatore)
  2. Proprietà dell'iniezione
  3. Metodo di iniezione

Esempio di installazione senza 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?
}

Iniezione delle dipendenze dell'inizializzatore

Come dice il nome, tutte le dipendenze vengono iniettate attraverso l'inizializzatore della classe. Per iniettare le dipendenze attraverso l'inizializzatore, aggiungeremo l'inizializzatore alla classe Train .

Il corso di formazione ora si presenta così:

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

Quando vogliamo creare un'istanza della classe Train utilizzeremo l'inizializzatore per iniettare una specifica implementazione del motore:

let train = Train(engine: TrainEngine())

NOTA: il vantaggio principale dell'iniezione dell'inizializzatore rispetto all'iniezione di proprietà è che possiamo impostare la variabile come variabile privata o addirittura renderla costante con la parola chiave let (come abbiamo fatto nel nostro esempio). In questo modo possiamo assicurarci che nessuno possa accedervi o cambiarlo.

Iniezione delle dipendenze delle proprietà

L'utilizzo delle proprietà di DI è ancora più semplice con l'uso di un inizializzatore. Inseriamo una dipendenza PassengerCar dall'oggetto del treno che abbiamo già creato utilizzando le proprietà DI:

train.mainCar = PassengerCar()

Questo è tutto. Il mainCar del nostro treno è ora un'istanza di PassengerCar .

Metodo Iniezione di dipendenza

Questo tipo di iniezione delle dipendenze è un po 'diverso dai precedenti perché non influenzerà l'intero oggetto, ma verrà iniettato solo una dipendenza da utilizzare nell'ambito di uno specifico metodo. Quando una dipendenza viene utilizzata solo in un singolo metodo, di solito non è buono per rendere l'intero oggetto dipendente da esso. Aggiungiamo un nuovo metodo alla classe Train:

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

Ora, se chiamiamo il metodo della nuova classe del treno, inietteremo la TrainCar usando l'iniezione della dipendenza del metodo.

train.reparkCar(trainCar: RestaurantCar())


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow