Sök…


Beroende på injektion med View Controllers

Dependenct injektionsintroduktion

En applikation består av många objekt som samarbetar med varandra. Objekt beror vanligtvis på andra objekt för att utföra någon uppgift. När ett objekt är ansvarigt för att hänvisa till sina egna beroenden leder det till en mycket kopplad, svår att testa och svår att ändra kod.

Dependency-injektion är ett mönster för mjukvarudesign som implementerar inversion av kontroll för att lösa beroenden. En injektion överförs av beroende till ett beroende objekt som skulle använda det. Detta gör det möjligt att skilja klientens beroenden från klientens beteende, vilket gör att applikationen kan kopplas löst.

För att inte förväxla med ovanstående definition - en beroendeinjektion betyder helt enkelt att ge ett objekt sina instansvariabler.

Det är så enkelt, men det ger många fördelar:

  • lättare att testa din kod (med automatiska tester som enhet och UI-test)
  • när det används tillsammans med protokollorienterad programmering gör det det enkelt att ändra implementeringen av en viss klass - lättare att refaktorera
  • det gör koden mer modulär och återanvändbar

Det finns tre vanligaste sätt Dependency Injection (DI) kan implementeras i en applikation:

  1. Initialiseringsinjektion
  2. Fastighetsinsprutning
  3. Använda tredjeparts DI-ramverk (som Swinject, Cleanse, Dip eller Typhoon)

Det finns en intressant artikel med länkar till fler artiklar om Dependency Injection så kolla in om du vill gräva djupare i DI och Inversion of Control-principen.

Låt oss visa hur man använder DI med View Controllers - en daglig uppgift för en genomsnittlig iOS-utvecklare.

Exempel utan DI

Vi har två View Controllers: LoginViewController och TimelineViewController . LoginViewController används för att logga in och vid lyckad loign kommer den att växla till TimelineViewController. Båda visningskontrollerna är beroende av 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
    }
}

Detta exempel är mycket enkelt, men låt oss anta att du har 10 eller 15 olika visningskontroller och några av dem är också beroende av FirebaseNetworkService. Vid ett ögonblick vill du ändra Firebase som din backend-tjänst med ditt företags interna backend-tjänst. För att göra det måste du gå igenom alla visningskontrollanter och byta FirebaseNetworkService med CompanyNetworkService. Och om några av metoderna i CompanyNetworkService har förändrats har du mycket arbete att göra.

Enhet- och UI-testning är inte omfattningen av detta exempel, men om du ville testa enhetskontroller med tätt kopplade beroenden skulle du ha svårt att göra det.

Låt oss skriva om detta exempel och injicera Network Service till våra synkontroller.

Exempel med beroende av injektion

För att göra det bästa av beroendeinsprutningen, låt oss definiera funktionen för nätverkstjänsten i ett protokoll. På det här sättet behöver visningskontrollanter som är beroende av en nätverkstjänst inte ens veta om den verkliga implementeringen av den.

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

Lägg till en implementering av NetworkService-protokollet:

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

Låt oss ändra LoginViewController och TimelineViewController för att använda ett nytt NetworkService-protokoll istället för 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()
    }
}

Frågan är nu: Hur injicerar vi rätt NetworkService-implementering i LoginViewController och TimelineViewController?

Eftersom LoginViewController är startvyskontrollern och kommer att visas varje gång applikationen startar, kan vi injicera alla beroenden i 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
}

I AppDelegate tar vi helt enkelt hänvisningen till den första visningskontrollern (LoginViewController) och injicerar NetworkService-implementeringen med hjälp av metoden för injektion av egendom.

Nu är nästa uppgift att injicera NetworkService-implementeringen i TimelineViewController. Det enklaste sättet är att göra det när LoginViewController övergår till TimlineViewController.

Vi lägger till injektionskoden i metoden PreparForSegue i LoginViewController (om du använder en annan metod för att navigera genom visningskontroller, placera injektionskoden där).

Vår LoginViewController-klass ser ut så här nu:

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

Vi är klara och det är så enkelt.

Föreställ dig nu att vi vill byta vår NetworkService-implementering från Firebase till vårt anpassade företags backendimplementering. Allt vi skulle behöva göra är:

Lägg till en ny implementeringsklass för NetworkService:

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

Växla FirebaseNetworkServiceImpl med den nya implementeringen i 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
    }

Det är det, vi har ändrat hela underlagsimplementeringen av NetworkService-protokollet utan att ens beröra LoginViewController eller TimelineViewController.

Eftersom detta är ett enkelt exempel kanske du inte ser alla fördelarna just nu, men om du försöker använda DI i dina projekt ser du fördelarna och kommer alltid att använda Dependency Injection.

Beroende på injektionstyper

Det här exemplet kommer att visa hur man använder Dependency Injection ( DI ) designmönster i Swift med hjälp av dessa metoder:

  1. Initialiseringsinjektion (den rätta termen är Constructor Injection, men eftersom Swift har initialiserare kallas det initieringsinjektion)
  2. Fastighetsinsprutning
  3. Metodinjektion

Exempelinställning utan 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?
}

Initialiseringsberoende injektion

Som namnet säger injiceras alla beroenden genom klassinitieraren. För att injicera beroenden genom initieraren lägger vi till initieraren till Train .

Tågklass ser nu ut så här:

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

När vi vill skapa en instans av tågklassen använder vi initialiseraren för att injicera en specifik motorimplementering:

let train = Train(engine: TrainEngine())

OBS: Huvudfördelen med initieringsinjektionen jämfört med injektionsinjektionen är att vi kan ställa in variabeln som privatvariabel eller till och med göra den konstant med let nyckelordet (som vi gjorde i vårt exempel). På detta sätt kan vi se till att ingen har åtkomst till det eller ändra det.

Egenskaper Beroende injektion

DI med egenskaper är ännu enklare än att använda en initialisator. Låt oss injicera ett PassengerCar-beroende till det tågobjekt vi redan skapat med egenskaperna DI:

train.mainCar = PassengerCar()

Det är allt. Vårt tåg mainCar är nu en PassengerCar instans.

Metod Beroende Injektion

Den här typen av beroendeinjektion är lite annorlunda än de föregående två eftersom det inte kommer att påverka hela objektet, men det kommer bara att injicera ett beroende som ska användas inom ramen för en specifik metod. När ett beroende endast används på en enda metod är det vanligtvis inte bra att göra hela objektet beroende av det. Låt oss lägga till en ny metod i tågklassen:

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

Om vi nu kallar den nya TrainCar injicerar vi TrainCar med metoden beroende injektion.

train.reparkCar(trainCar: RestaurantCar())


Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow