Suche…


Abhängigkeitseinspritzung mit View-Controllern

Abhängigkeit Injection Intro

Eine Anwendung besteht aus vielen Objekten, die miteinander zusammenarbeiten. In der Regel hängen Objekte von anderen Objekten ab, um eine Aufgabe auszuführen. Wenn ein Objekt für das Referenzieren seiner eigenen Abhängigkeiten verantwortlich ist, führt dies zu einem stark gekoppelten, schwer zu testenden und schwer zu ändernden Code.

Abhängigkeitsinjektion ist ein Software-Entwurfsmuster, das eine Umkehrung der Steuerung zur Auflösung von Abhängigkeiten implementiert. Eine Injektion übergibt die Abhängigkeit an ein abhängiges Objekt, das sie verwenden würde. Dies ermöglicht eine Trennung der Abhängigkeiten des Kunden vom Verhalten des Kunden, wodurch die Anwendung lose gekoppelt werden kann.

Nicht mit der obigen Definition zu verwechseln - eine Abhängigkeitseingabe bedeutet einfach, einem Objekt seine Instanzvariablen zu geben.

So einfach ist es, aber es bietet viele Vorteile:

  • einfacher, Ihren Code zu testen (mit automatisierten Tests wie Unit- und UI-Tests)
  • In Kombination mit der protokollorientierten Programmierung können Sie die Implementierung einer bestimmten Klasse leicht ändern - und können sie leichter umgestalten
  • Dadurch wird der Code modularer und wiederverwendbarer

Es gibt drei am häufigsten verwendete Möglichkeiten, die Abhängigkeitsinjektion (DI) in einer Anwendung zu implementieren:

  1. Initialisierer-Injektion
  2. Immobilieninjektion
  3. Verwenden von DI-Frameworks von Drittanbietern (wie Swinject, Cleanse, Dip oder Typhoon)

Es gibt einen interessanten Artikel mit Links zu weiteren Artikeln zum Thema Abhängigkeitsinjektion. Schauen Sie sich also an, ob Sie tiefer in das DI- und Inversion-of-Control-Prinzip einsteigen möchten.

Lassen Sie uns zeigen, wie Sie DI mit View Controllern verwenden - eine tägliche Aufgabe für einen durchschnittlichen iOS-Entwickler.

Beispiel ohne DI

Wir haben zwei View-Controller: LoginViewController und TimelineViewController . LoginViewController wird verwendet, um sich anzumelden, und bei erfolgreicher Anmeldung wird auf TimelineViewController umgeschaltet. Beide Ansichtscontroller sind vom FirebaseNetworkService abhängig.

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

Dieses Beispiel ist sehr einfach. Nehmen wir jedoch an, Sie haben 10 oder 15 verschiedene Ansichtscontroller, von denen einige auch vom FirebaseNetworkService abhängig sind. Irgendwann möchten Sie Firebase als Backend-Service mit dem In-House-Backend-Service Ihres Unternehmens ändern. Dazu müssen Sie jeden View-Controller durchgehen und FirebaseNetworkService mit CompanyNetworkService ändern. Und wenn sich einige der Methoden im CompanyNetworkService geändert haben, müssen Sie viel Arbeit erledigen.

Das Testen von Einheiten und Benutzeroberflächen ist nicht Gegenstand dieses Beispiels. Wenn Sie jedoch Ansichtssteuerungen mit eng gekoppelten Abhängigkeiten zusammenstellen möchten, wäre dies sehr schwierig.

Lassen Sie uns dieses Beispiel neu schreiben und Network View in unsere View-Controller einbinden.

Beispiel mit Abhängigkeitsinjektion

Um das Beste aus der Abhängigkeitseinspritzung herauszuholen, definieren wir die Funktionalität des Netzwerkdienstes in einem Protokoll. Auf diese Weise müssen View Controller, die von einem Netzwerkdienst abhängig sind, nicht einmal die tatsächliche Implementierung kennen.

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

Fügen Sie eine Implementierung des NetworkService-Protokolls hinzu:

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

Ändern wir nun LoginViewController und TimelineViewController, um das neue NetworkService-Protokoll anstelle von FirebaseNetworkService zu verwenden.

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

Nun stellt sich die Frage: Wie fügen wir die korrekte NetworkService-Implementierung in den LoginViewController und den TimelineViewController ein?

Da LoginViewController der Start-View-Controller ist und bei jedem Start der Anwendung angezeigt wird, können wir alle Abhängigkeiten in AppDelegate einfügen .

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 übernehmen wir einfach den Verweis auf den ersten View-Controller (LoginViewController) und injizieren die NetworkService-Implementierung mithilfe der Eigenschaftsinjektionsmethode.

Als nächstes müssen Sie die NetworkService-Implementierung in den TimelineViewController einfügen. Dies ist am einfachsten, wenn LoginViewController zum TimlineViewController wechselt.

Wir fügen den Injektionscode in der prepareForSegue-Methode im LoginViewController hinzu (wenn Sie einen anderen Ansatz zum Navigieren durch Ansichtssteuerungen verwenden, platzieren Sie den Injektionscode dort).

Unsere LoginViewController-Klasse sieht jetzt so aus:

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

Wir sind fertig und es ist so einfach.

Stellen Sie sich jetzt vor, wir möchten unsere NetworkService-Implementierung von Firebase auf die Back-End-Implementierung unseres benutzerdefinierten Unternehmens umstellen. Alles was wir tun müssen, ist:

Neue NetworkService-Implementierungsklasse hinzufügen:

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

Wechseln Sie das FirebaseNetworkServiceImpl mit der neuen Implementierung 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
    }

Wir haben die gesamte zugrunde liegende Implementierung des NetworkService-Protokolls umgestellt, ohne LoginViewController oder TimelineViewController zu berühren.

Da dies ein einfaches Beispiel ist, werden möglicherweise nicht alle Vorteile angezeigt. Wenn Sie versuchen, DI in Ihren Projekten zu verwenden, werden Sie die Vorteile sehen und immer die Abhängigkeitseinspritzung verwenden.

Abhängigkeitseinspritzarten

In diesem Beispiel wird veranschaulicht, wie das Abhängigkeitsmuster (Dependency Injection, DI ) in Swift mit den folgenden Methoden verwendet wird:

  1. Initializer-Injektion (der richtige Begriff ist Konstruktor-Injektion, aber da Swift Initialisierer hat, wird dies als Initializer-Injektion bezeichnet.)
  2. Immobilieninjektion
  3. Methode Injection

Beispiel Setup ohne 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 Dependency Injection

Wie der Name schon sagt, werden alle Abhängigkeiten durch den Klasseninitialisierer eingefügt. Um Abhängigkeiten über den Initialisierer einzufügen, fügen wir den Initialisierer zur Train Klasse hinzu.

Der Trainenkurs sieht jetzt so aus:

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

Wenn wir eine Instanz der Train-Klasse erstellen möchten, verwenden wir den Initialisierer, um eine bestimmte Engine-Implementierung einzufügen:

let train = Train(engine: TrainEngine())

HINWEIS: Der Hauptvorteil der Initializer-Injektion gegenüber der Eigenschaftsinjektion besteht darin, dass wir die Variable als private Variable festlegen oder sogar mit dem Schlüsselwort let einer Konstante machen können (wie in unserem Beispiel). Auf diese Weise können wir sicherstellen, dass niemand darauf zugreifen oder sie ändern kann.

Eigenschaften Abhängigkeitsinjektion

DI mit Eigenschaften ist noch einfacher als mit einem Initialisierer. Fügen Sie dem von uns erstellten Zugobjekt eine PassengerCar-Abhängigkeit mit den Eigenschaften DI hinzu:

train.mainCar = PassengerCar()

Das ist es. Das mainCar unseres Zuges ist jetzt eine PassengerCar Instanz.

Methodenabhängigkeit Injection

Diese Art der Abhängigkeitsinjektion unterscheidet sich ein wenig von den beiden vorherigen, da sie nicht das gesamte Objekt betrifft, sondern nur eine Abhängigkeit enthält, die im Rahmen einer bestimmten Methode verwendet werden soll. Wenn eine Abhängigkeit nur in einer einzelnen Methode verwendet wird, ist es normalerweise nicht gut, das gesamte Objekt davon abhängig zu machen. Fügen wir der Train-Klasse eine neue Methode hinzu:

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

Wenn wir nun die Klassenmethode des neuen Train aufrufen, injizieren wir den TrainCar mit der Methode Dependency Injection.

train.reparkCar(trainCar: RestaurantCar())


Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow