it-swarm.com.de

Abhängigkeitsinjektion mit Swift mit Abhängigkeitsgraph von zwei UIViewControllern ohne gemeinsames übergeordnetes Element

Wie können wir Abhängigkeitsinjektion anwenden, ohne ein Framework zu verwenden, wenn wir zwei UIViewControllers haben, die sich sehr tief in der Hierarchie befinden und beide dieselbe Abhängigkeit benötigen, die den Status "state" hat, und dass diese beiden UIViewControllers keine gemeinsamen übergeordneten Elemente haben?.

Beispiel: 

VC1 -> VC2 -> VC3 -> VC4

VC5 -> VC6 -> VC7 -> VC8 

nehmen wir an, dass VC4 und VC8 beide UserService benötigen, die den aktuellen Benutzer enthält. 

Beachten Sie, dass wir Singleton vermeiden möchten.

Gibt es eine elegante Art und Weise, mit solchen DI-Situationen umzugehen? 

Nach einiger Recherche fand ich heraus, dass einige Abstract Factory, Context interfaces, Builder, strategy pattern erwähnen. 

Ich konnte jedoch kein Beispiel finden, wie ich das auf iOS anwenden kann 

8
iOSGeek

Okay, ich werde es versuchen.

Sie sagten "kein Singleton", deshalb schließe ich das im Folgenden aus, aber sehen Sie sich bitte auch den unteren Teil dieser Antwort an.

Josh Homanns Kommentar ist bereits ein guter Hinweis auf eine Lösung, aber ich persönlich habe meine Probleme mit dem Koordinatormuster.

Wie Josh richtig sagte, sollten View Controller nicht (viel) voneinander wissen [1], aber wie ist dann z. ein Koordinator oder eine Abhängigkeit herumgereicht/zugegriffen? Es gibt verschiedene Muster, die darauf hindeuten, wie dies geschehen soll, aber die meisten haben ein Problem, das grundsätzlich Ihren Anforderungen zuwiderläuft: Sie machen den Koordinator mehr oder weniger zu einem Singleton (entweder selbst oder als Eigenschaft eines anderen Singletons wie dem AppDelegate). Ein Koordinator ist oftmals auch ein Singleton (aber nicht immer und muss es auch nicht sein).

Ich neige dazu, mich auf einfache initialisierte Eigenschaften oder (meistens) faule Eigenschaften und protokollorientierte Programmierung zu verlassen. Lassen Sie uns ein Beispiel konstruieren: UserService soll das Protokoll sein, das alle Funktionen definiert, die Ihr Dienst benötigt, MyUserService seine Implementierungsstruktur. Angenommen, UserService ist ein Designkonstrukt, das im Wesentlichen als Get-/Set-System für einige benutzerbezogene Daten fungiert: Zugriffstoken (z. B. im Schlüsselbund gespeichert), einige Einstellungen (URL eines Avatar-Bilds) und dergleichen. Bei der Initialisierung bereitet MyUserService auch die Daten auf (z. B. das Laden von der Fernbedienung). Dies soll in mehreren unabhängigen Bildschirmen/View Controllern verwendet werden und ist kein Singleton.

Jetzt hat jeder View-Controller, der auf diese Daten zugreifen möchte, eine einfache Eigenschaft:

lazy var userService: UserService = MyUserService()

Ich halte es öffentlich, weil ich es so in Unit-Tests leicht verspotten/stubben kann (wenn ich das tun muss, kann ich einen Dummy TestUserService erstellen, der das Verhalten verspottet/stubbt). Die Instanziierung könnte auch ein Abschluss sein, den ich während eines Tests leicht ausschalten kann, wenn der Init Parameter benötigt. Offensichtlich müssen die Eigenschaften nicht unbedingt lazy sein, je nachdem, was die Objekte tatsächlich tun. Wenn es nicht schadet, das Objekt vorab zu instanziieren (Unit-Tests beachten, auch ausgehende Verbindungen), überspringen Sie einfach das lazy.

Der Trick besteht offensichtlich darin, UserService und/oder MyUserService so zu gestalten, dass beim Erstellen mehrerer Instanzen keine Probleme auftreten =. Ich stellte jedoch fest, dass dies in 90% der Fälle kein Problem ist, solange die eigentlichen Daten, auf die sich die Instanz stützen soll, an einem anderen Ort gespeichert sind, an einem einzigen Punkt, wie dem Schlüsselbund, einem zentralen Datenstapel. Benutzervorgaben oder ein Remote-Backend.

Mir ist bewusst, dass dies eine Art Antwort ist, die sich aus dem Ruder läuft, da ich hier nur einen Ansatz beschreibe, der (zumindest teilweise) aus vielen allgemeinen Mustern besteht. Ich fand jedoch, dass dies die allgemeinste und einfachste Form ist, um sich der Abhängigkeitsinjektion in Swift zu nähern. Das Koordinatormuster kann orthogonal dazu verwendet werden, aber ich fand, dass es im täglichen Gebrauch weniger "apfelartig" ist. Es löst zwar ein Problem, aber meistens verwenden Sie Storyboards nicht ordnungsgemäß, wie Sie es möchten (insbesondere: Verwenden Sie sie nur als "VC-Repos", instanziieren Sie sie von dort und setzen Sie sich in Code um).

[1] Mit Ausnahme einiger grundlegender und/oder geringfügiger Dinge, die Sie in einem Completion-Handler oder in prepareForSegue übergeben können. Das ist fraglich und hängt davon ab, wie streng Sie dem Koordinator oder einem anderen Muster folgen. Persönlich nehme ich hier manchmal eine Abkürzung, solange es die Dinge nicht aufbläht und chaotisch wird. Einige Popup-Designs sind auf diese Weise einfacher zu erstellen.


Als Schlussbemerkung erwecken der Satz "Beachten Sie, dass wir Singleton vermeiden wollen" sowie Ihr Kommentar dazu unter der Frage den Eindruck, dass Sie diesem Rat einfach folgen, ohne über die Gründe nachgedacht zu haben. Ich weiß, dass "Singleton" oft als Anti-Pattern angesehen wird, aber genauso oft ist dieses Urteil falsch informiert. Ein Singleton kann ein gültiges Architekturkonzept sein (was daran zu erkennen ist, dass es in Frameworks und Bibliotheken häufig verwendet wird). Das Schlimme daran ist, dass Entwickler zu oft versucht werden, Verknüpfungen im Design zu verwenden und sie als eine Art "Objekt-Repository" zu missbrauchen, sodass sie nicht darüber nachdenken müssen, wann und wo Objekte instanziiert werden sollen. Dies führt zu Unordnung und dem schlechten Ruf des Musters.

Ein UserService, abhängig davon, was das in Ihrer App tatsächlich tut , könnte ein guter Kandidat für einen Singleton sein. Meine persönliche Faustregel lautet: "Wenn es den Zustand von etwas verwaltet, das einzigartig und einzigartig ist, wie ein bestimmter Benutzer, der sich zu einem bestimmten Zeitpunkt immer nur in einem Zustand befinden kann", könnte ich für einen Singleton gehen.

Insbesondere wenn Sie es nicht wie oben beschrieben entwerfen können, dh , wenn Sie speicherinterne, singuläre Zustandsdaten benötigen , ist ein Singleton im Grunde genommen ein einfache und richtige Möglichkeit, dies umzusetzen. (Auch wenn die Verwendung von (Lazy) -Eigenschaften von Vorteil ist, müssen Ihre View-Controller nicht einmal wissen, ob es sich um einen Singleton handelt oder nicht, und Sie können ihn dennoch einzeln stoppen/verspotten (d. H. Nicht nur die globale Instanz).)

6
Gero

Dies sind Ihre Anforderungen, so wie ich sie verstehe:

  1. VC4 und VC8 müssen den Status über eine UserService-Klasse gemeinsam nutzen können.
  2. UserService darf kein Singleton sein.
  3. UserService muss über Abhängigkeitseinspritzung an VC4 und VC8 übergeben werden.
  4. Ein Abhängigkeitsinjektions-Framework darf nicht verwendet werden.

Im Rahmen dieser Einschränkungen würde ich den folgenden Ansatz vorschlagen.

Definieren Sie eine UserServiceProtocol, die über Methoden und/oder Eigenschaften verfügt, um auf den Status zuzugreifen und ihn zu aktualisieren. Zum Beispiel:

protocol UserServiceProtocol {
    func login(user: String, password: String) -> Bool
    func logout()
    var loggedInUser: User? //where User is some model you define
}

Definieren Sie eine UserService-Klasse, die das Protokoll implementiert und irgendwo seinen Status speichert. 

Wenn der Status nur so lange andauern muss, wie die App ausgeführt wird, können Sie den Status in einer bestimmten Instanz speichern. Diese Instanz muss jedoch zwischen VC4 und VC8 geteilt werden. 

In diesem Fall würde ich empfehlen, die Instanz in AppDelegate zu erstellen, zu halten und durch die Kette der virtuellen Controller zu führen. 

Wenn der Status zwischen den Startvorgängen der App bestehen bleiben muss oder Sie keine Instanz durch die Kette der VCs übergeben möchten, können Sie den Status in Benutzervorgaben, Core Data, Realm oder einer beliebigen Anzahl von Orten außerhalb speichern die Klasse selbst.

In diesem Fall können Sie die Variable UserService in VC3 und VC7 erstellen und an VC4 und VC8 übergeben. VC4 und VC8 hätten var userService: UserServiceProtocol?. Die Variable UserService müsste ihren Status von der externen Quelle wiederherstellen. Obwohl VC4 und VC8 unterschiedliche Instanzen des Objekts haben, wäre der Zustand auf diese Weise derselbe.

3
Mike Taverne

Zunächst glaube ich, dass Ihre Frage falsch ist.

Sie definieren Ihre VC'c-Hierarchie als solche:

Beispiel:

VC1 -> VC2 -> VC3 -> VC4

VC5 -> VC6 -> VC7 -> VC8

Unter iOS (es sei denn, Sie verwenden einige sehr seltsame Hacks), wird es zu einem bestimmten Zeitpunkt immer ein gemeinsames übergeordnetes Element sein, z. B. ein Navigations-Controller, ein Registerkarten-Controller, ein Master-Detail-Controller oder ein Page View-Controller.

Ich gehe also davon aus, dass ein korrektes Schema beispielsweise so aussehen könnte:

Registerleiste Controller 1 -> Navigationscontroller 1 -> VC1 -> VC2 -> VC3 -> VC4

Registerleiste Controller 1 -> Navigationscontroller 2 -> VC5 -> VC6 -> VC7 -> VC8

Ich glaube, wenn man es so sieht, ist es leicht, deine Frage zu beantworten.

Wenn Sie jetzt nach einer Meinung fragen, wie Sie mit DI unter iOS am besten umgehen können, würde ich sagen, dass es nicht den besten Weg gibt. Ich persönlich halte mich jedoch gerne an die Regel, dass Objekte nicht für ihre eigene Erstellung/Initialisierung verantwortlich sein sollten. So Dinge wie

private lazy var service: SomeService = SomeService()

sind außer Frage. Ich würde eine Init bevorzugen, die eine SomeService-Instanz erfordert oder zumindest (einfach für ViewControllers):

var service: SomeService!

Auf diese Weise übergeben Sie die Verantwortung für das Abrufen der richtigen Modelle/Dienste usw. an den Ersteller der Instanz. Inzwischen können Sie Ihre Logik mit einer einfachen, aber wichtigen Annahme implementieren, dass Sie über alles verfügen, was Sie benötigen (oder Ihre Klasse früh zum Scheitern verurteilt) (zum Beispiel durch Verwendung von Force-Wrapping), was während der Entwicklung tatsächlich gut ist).

Nun, wie Sie diese Modelle abrufen - indem Sie sie initialisieren, weitergeben, einen Singleton verwenden, Provider, Container, Koordinatoren usw. verwenden -, liegt es ganz bei Ihnen und sollte auch von Faktoren wie der Komplexität des Projekts und den Kundenanforderungen abhängen Egal welche Tools Sie verwenden - im Allgemeinen ist alles gut, solange Sie sich an die guten OOP -Praktiken halten.

2
user3581248

Hier ist ein Ansatz, den ich bei einigen Projekten verwendet habe, der Ihnen helfen könnte.

  1. Erstellen Sie alle View-Controller über Factory-Methoden in einer ViewControllerFactory.
  2. Die ViewControllerFactory verfügt über ein eigenes UserService-Objekt.
  3. Übergeben Sie das UserService-Objekt des ViewControllerFactory an die View-Controller, die es benötigen.

Ein bescheidenes Beispiel hier:

struct ViewControllerFactory {

private let userService: UserServiceProtocol

init(userService: UserServiceProtocol) {
    self.userService = userService
}

// This VC needs the user service
func makeVC4() -> VC4 {
    let vc4 = VC4(userService: userService)
    return vc4
}

// This VC does not
func makeVC5() -> VC5 {
    let vc5 = VC5()
}

// This VC also needs the user service
func makeVC8() -> VC8 {
    let vc8 = VC8(userService: userService)
    return vc8
}
}  

Das ViewControllerFactory-Objekt kann instanziiert und in AppDelegate gespeichert werden.

Das sind die Grundlagen. Darüber hinaus würde ich auch folgendes betrachten (siehe auch die anderen Antworten, die hier einige gute Vorschläge gemacht haben):

  1. Erstellen Sie ein UserServiceProtocol, mit dem UserService übereinstimmt. Dies macht es einfach, Scheinobjekte zum Testen zu erstellen.
  2. Sehen Sie sich das Koordinatemuster an, um die Navigationslogik zu behandeln.
2
Markk
let viewController = CustomViewController()
viewController.data = NSObject() //some data object
navigationController.show(viewController, sender: self)


import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var appCoordinator:AppCoordinator?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = UINavigationController()
        appCoordinator = AppCoordinator(with: window?.rootViewController as! UINavigationController)
        appCoordinator?.start()
        window?.makeKeyAndVisible()
        return true
    }
}
0
Sachin S

Ich finde, dass das Koordinator/Router-Entwurfsmuster am besten geeignet ist, um Abhängigkeiten einzufügen und die App-Navigation zu handhaben. Schauen Sie sich diesen Beitrag an, er hat mir sehr geholfen https://medium.com/@dkw5877/flow-coordinators-333ed64f3dd

0
andrei

Ich habe versucht, dieses Problem zu lösen und eine Beispielarchitektur hier hochgeladen: https://github.com/ivanovi/DI-demo

Um es klarer zu machen, habe ich die Implementierung mit drei VCs vereinfacht, aber die Lösung funktioniert mit jeder Tiefe. Die View Controller-Kette sieht wie folgt aus:

Master -> Detail -> MoreDetail (wo die Abhängigkeit eingefügt wird)

Die vorgeschlagene Architektur umfasst vier Bausteine:

  • Koordinator-Repository: Enthält alle Koordinatoren und die gemeinsamen Zustände. Spritzt die erforderlichen Abhängigkeiten ein.

  • ViewController Coordinator: Führt die Navigation zum nächsten ViewController aus. Der Koordinator besitzt eine Fabrik, die die nächste nächste Instanz eines VC erstellt. 

  • ViewController Factory: Verantwortlich für die Initialisierung und Konfiguration eines bestimmten ViewControllers. Es gehört normalerweise einem Koordinator und wird vom CoordinatorRepository in den Koordinator eingefügt.

  • Der ViewController: Der ViewController, der auf dem Bildschirm dargestellt werden soll.

N.b .: In dem Beispiel gebe ich die neu erstellte VC Instanz zurück, nur um das Beispiel zu erzeugen - d. H. In der realen Implementierung ist das Zurückgeben von VC nicht erforderlich.

Ich hoffe es hilft.

0
Ivan S Ivanov