Zoeken…


Afhankelijkheidsinjectie met View Controllers

Dependenct Injectie Intro

Een applicatie bestaat uit vele objecten die met elkaar samenwerken. Objecten zijn meestal afhankelijk van andere objecten om een taak uit te voeren. Wanneer een object verantwoordelijk is voor het verwijzen naar zijn eigen afhankelijkheden, leidt dit tot een sterk gekoppelde, moeilijk te testen en moeilijk te wijzigen code.

Afhankelijkheid injectie is een software-ontwerppatroon dat inversie van controle implementeert voor het oplossen van afhankelijkheden. Een injectie gaat van afhankelijkheid over naar een afhankelijk object dat het zou gebruiken. Hierdoor kan de afhankelijkheid van de klant worden gescheiden van het gedrag van de klant, waardoor de toepassing losjes kan worden gekoppeld.

Niet te verwarren met de bovenstaande definitie - een afhankelijkheidsinjectie betekent eenvoudigweg een object zijn instantievariabelen geven.

Het is zo simpel, maar het biedt veel voordelen:

  • gemakkelijker om uw code te testen (met behulp van geautomatiseerde tests zoals unit- en UI-tests)
  • in combinatie met protocol-georiënteerd programmeren maakt het het gemakkelijk om de implementatie van een bepaalde klasse te wijzigen - gemakkelijker te refactoren
  • het maakt de code modulair en herbruikbaar

Er zijn drie meest gebruikte manieren waarop Dependency Injection (DI) in een toepassing kan worden geïmplementeerd:

  1. Initializer injectie
  2. Eigendom injectie
  3. DI-frameworks van derden gebruiken (zoals Swinject, Cleanse, Dip of Typhoon)

Er is een interessant artikel met links naar meer artikelen over Dependency Injection, dus bekijk het als je dieper wilt ingaan op DI en het Inversion of Control-principe.

Laten we laten zien hoe DI te gebruiken met View Controllers - een dagelijkse taak voor een gemiddelde iOS-ontwikkelaar.

Voorbeeld zonder DI

We hebben twee View Controllers: LoginViewController en TimelineViewController . LoginViewController wordt gebruikt om in te loggen en bij succesvolle loign, zal het overschakelen naar de TimelineViewController. Beide viewcontrollers zijn afhankelijk van 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
    }
}

Dit voorbeeld is heel eenvoudig, maar laten we aannemen dat u 10 of 15 verschillende view-controllers hebt en dat sommige daarvan ook afhankelijk zijn van de FirebaseNetworkService. Op een gegeven moment wilt u Firebase veranderen als uw backend-service met de interne backend-service van uw bedrijf. Om dat te doen, moet u elke viewcontroller doorlopen en FirebaseNetworkService wijzigen met CompanyNetworkService. En als enkele van de methoden in de CompanyNetworkService zijn gewijzigd, moet u nog veel werk verzetten.

Unit- en UI-testen vallen niet onder dit voorbeeld, maar als u viewview-controllers met nauw gekoppelde afhankelijkheden wilt testen, zou u het heel moeilijk hebben om dit te doen.

Laten we dit voorbeeld herschrijven en Network Service injecteren in onze viewcontrollers.

Voorbeeld met afhankelijke injectie

Laten we de functionaliteit van de netwerkservice in een protocol definiëren om het beste uit de afhankelijkheidsinjectie te halen. Op deze manier hoeven viewcontrollers die afhankelijk zijn van een netwerkdienst niet eens te weten over de echte implementatie ervan.

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

Voeg een implementatie van het NetworkService-protocol toe:

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

Laten we LoginViewController en TimelineViewController wijzigen om een nieuw NetworkService-protocol te gebruiken in plaats van 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()
    }
}

De vraag is nu: hoe injecteren we de juiste NetworkService-implementatie in de LoginViewController en TimelineViewController?

Omdat LoginViewController de startweergavecontroller is en elke keer wordt weergegeven wanneer de toepassing wordt gestart, kunnen we alle afhankelijkheden in de AppDelegate injecteren .

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 de AppDelegate nemen we eenvoudigweg de verwijzing naar de first view-controller (LoginViewController) en injecteren we de NetworkService-implementatie met behulp van de methode voor injectie van de eigenschap.

Nu is de volgende taak om de NetworkService-implementatie in de TimelineViewController te injecteren. De eenvoudigste manier is om dat te doen wanneer LoginViewController overgaat naar de TimlineViewController.

We voegen de injectiecode toe in de methode PrepForSegue in de LoginViewController (als u een andere aanpak gebruikt om door viewcontrollers te navigeren, plaatst u de injectiecode daar).

Onze LoginViewController-klasse ziet er nu zo uit:

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

We zijn klaar en zo gemakkelijk is het.

Stel je nu voor dat we onze NetworkService-implementatie willen overschakelen van Firebase naar de backend-implementatie van ons aangepaste bedrijf. Het enige dat we zouden moeten doen is:

Nieuwe NetworkService-implementatieklasse toevoegen:

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

Schakel de FirebaseNetworkServiceImpl met de nieuwe implementatie in de 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
    }

Dat is alles, we hebben de hele onderliggende implementatie van het NetworkService-protocol verwisseld zonder LoginViewController of TimelineViewController aan te raken.

Omdat dit een eenvoudig voorbeeld is, ziet u mogelijk niet alle voordelen op dit moment, maar als u DI in uw projecten probeert te gebruiken, ziet u de voordelen en gebruikt u altijd afhankelijkheidsinjectie.

Afhankelijkheid Injectietypen

Dit voorbeeld laat zien hoe het ontwerppatroon Dependency Injection ( DI ) in Swift kan worden gebruikt met behulp van deze methoden:

  1. Initializer-injectie (de juiste term is Constructor Injection, maar omdat Swift initializers heeft, wordt dit initializer-injectie genoemd)
  2. Eigendom injectie
  3. Methode injectie

Voorbeeldinstelling zonder 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?
}

Initializer afhankelijkheid injectie

Zoals de naam al zegt, worden alle afhankelijkheden geïnjecteerd via de initialisatie van de klasse. Om afhankelijkheden via de initialisatie te injecteren, voegen we de initialisatie toe aan de klasse Train .

Train class ziet er nu zo uit:

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

Wanneer we een instantie van de klasse Train willen maken, gebruiken we initialisatie om een specifieke Engine-implementatie te injecteren:

let train = Train(engine: TrainEngine())

OPMERKING: Het belangrijkste voordeel van de initialisatie-injectie ten opzichte van de eigenschapsinjectie is dat we de variabele als privévariabele kunnen instellen of zelfs een constante kunnen maken met het sleutelwoord let (zoals in ons voorbeeld). Op deze manier kunnen we ervoor zorgen dat niemand er toegang toe heeft of het kan wijzigen.

Eigenschappen Afhankelijkheid Injectie

DI met eigenschappen is nog eenvoudiger dan met een initialisatie. Laten we een PassengerCar-afhankelijkheid van het treinobject dat we al hebben gemaakt, injecteren met de eigenschappen DI:

train.mainCar = PassengerCar()

Dat is het. De mainCar onze trein is nu een PassengerCar instantie.

Methode Afhankelijkheid Injectie

Dit type afhankelijkheidsinjectie is een beetje anders dan de vorige twee omdat het niet het hele object beïnvloedt, maar het zal alleen een afhankelijkheid injecteren die in het kader van een specifieke methode moet worden gebruikt. Wanneer een afhankelijkheid alleen in een enkele methode wordt gebruikt, is het meestal niet goed om het hele object ervan afhankelijk te maken. Laten we een nieuwe methode toevoegen aan de klasse Train:

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

Als we nu de nieuwe class-methode van Train aanroepen, injecteren we de TrainCar met behulp van de methode-afhankelijkheidsinjectie.

train.reparkCar(trainCar: RestaurantCar())


Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow