web-dev-qa-db-fra.com

Injection de dépendance avec Swift avec graphe de dépendance de deux contrôleurs UIView sans parent commun

Comment appliquer l'injection de dépendances sans utiliser Framework lorsque nous avons deux UIViewControllers très profonds dans la hiérarchie et qu'ils ont tous deux besoin de la même dépendance qui maintient l'état et que ces deux UIViewControllers n'ont pas de parent commun.

Exemple: 

VC1 -> VC2 -> VC3 -> VC4

VC5 -> VC6 -> VC7 -> VC8 

supposons que VC4 et VC8 ont tous deux besoin de UserService qui contient l'utilisateur actuel. 

Notez que nous voulons éviter Singleton.

Existe-t-il un moyen élégant de gérer ce type de situation? 

Après quelques recherches, j'ai trouvé que certains mentionnent Abstract Factory, Context interfaces, Builder, strategy pattern 

Mais je ne trouvais pas d'exemple sur la façon d'appliquer cela sur iOS 

8
iOSGeek

Ok, je vais essayer.

Vous avez dit "pas de singleton", je l’exclus donc, mais voyez aussi le bas de la réponse.

Le commentaire de Josh Homann est déjà un bon pointeur pour une solution, mais personnellement, j'ai des problèmes avec le motif de coordination. 

Comme Josh a correctement dit, les contrôleurs de vue ne devraient pas se connaître (beaucoup) l'un de l'autre [1], mais comment se fait-il, par exemple? un coordinateur ou une dépendance transmise/accédée? Plusieurs modèles suggèrent comment, mais la plupart ont un problème qui va fondamentalement à l’encontre de votre exigence: ils font plus ou moins du coordinateur un singleton (soit lui-même, soit en tant que propriété d’un autre singleton comme le AppDelegate). Un coordinateur est souvent aussi un singleton (mais pas toujours, et ce n’est pas forcément le cas).

Ce que j'ai tendance à faire, c'est de s'appuyer sur de simples propriétés initialisées ou (le plus souvent) des propriétés lazy et une programmation orientée protocole. Construisons un exemple: UserService doit être le protocole définissant toutes les fonctionnalités dont votre service a besoin, MyUserService sa structure d'implémentation. Supposons que UserService soit une structure de conception qui fonctionne essentiellement comme un système de lecture/définition pour certaines données relatives à l'utilisateur: jetons d'accès (par exemple, enregistrés dans le trousseau), certaines préférences (URL de l'image d'un avatar), etc. Lors de l'initialisation, MyUserService prépare également les données (par exemple, les chargements provenant de la télécommande). Ceci doit être utilisé dans plusieurs écrans/contrôleurs de vue indépendants et n'est pas un singleton.

Désormais, chaque contrôleur de vue intéressé à accéder à ces données possède une propriété simple:

lazy var userService: UserService = MyUserService()

Je le garde public parce que cela me permet de facilement simuler/stub dans des tests unitaires (si je dois le faire, je peux créer une variable TestUserService qui simule/stub le comportement). L'instanciation pourrait également être une fermeture que je peux facilement désactiver lors d'un test si l'init a besoin de paramètres. Évidemment, les propriétés n'ont même pas nécessairement besoin d'être lazy en fonction de ce que les objets font réellement. Si instancier l'objet à l'avance ne nuit pas (gardez à l'esprit les tests unitaires, ainsi que les connexions sortantes), ignorez simplement la lazy.

Le truc est évidemment de concevoir UserService et/ou MyUserService de manière à ne pas poser de problèmes lors de la création de plusieurs instances . Cependant, j’ai constaté que ce n’était pas vraiment un problème 90% du temps, tant que les données réelles sur lesquelles l’instance est supposée reposer sont sauvegardées ailleurs, en un point de vérité, comme le trousseau, une pile de données principale, les valeurs par défaut de l'utilisateur ou un serveur distant.

Je suis conscient que c'est en quelque sorte une solution de rechange, dans la mesure où je dis simplement décrire une approche qui fait (du moins en partie partie) de nombreux modèles génériques. Cependant, j’ai trouvé que c’était la forme la plus générique et la plus simple pour aborder l’injection de dépendance dans Swift. Le modèle de coordinateur peut être utilisé orthogonalement, mais j’ai trouvé qu’il ressemblait moins à "Apple" dans son utilisation quotidienne. Cela résout un problème, mais dans la plupart des cas, vous n'utilisez pas les storyboards tels qu'ils sont conçus (en particulier: utilisez-les simplement comme "repos VC", instanciez-les à partir de là et effectuez une transition en code).

[1] Sauf quelques éléments de base et/ou mineurs que vous pouvez transmettre à un gestionnaire d’achèvement ou à prepareForSegue. C'est discutable et dépend de la façon dont vous suivez le coordinateur ou un autre modèle. Personnellement, je prends parfois un raccourci ici, à condition que cela n'empoisonne pas les choses et ne devienne désordonné. Certaines conceptions contextuelles sont plus simples à réaliser de cette façon.


En guise de conclusion, le membre de phrase "Notez que nous voulons éviter Singleton", ainsi que votre commentaire à propos de la question, me donnent l’impression que vous suivez ce conseil sans avoir correctement réfléchi à la justification. Je sais que "Singleton" est souvent considéré comme un anti-modèle, mais ce jugement est aussi souvent mal informé. Un singleton peut être un concept architectural valide (ce que vous pouvez voir par le fait qu'il est largement utilisé dans les frameworks et les bibliothèques). Le problème, c’est que les développeurs sont trop souvent tentés de prendre des raccourcis dans la conception et de les utiliser comme une sorte de "référentiel d’objets" afin de ne pas avoir à se demander quand et où instancier des objets. Cela conduit à la confusion et à la mauvaise réputation du motif.

Un UserService, en fonction de ce que cela fait réellement dans votre application pourrait être un bon candidat pour un singleton. Ma règle personnelle est la suivante: "S'il gère l'état de quelque chose qui est singulier et unique, comme un utilisateur spécifique qui ne peut jamais être dans un état à un moment donné", je _ {pourrait choisir un singleton.

Surtout si vous ne pouvez pas le concevoir de la manière décrite ci-dessus, à savoir si vous avez besoin de données d'état en mémoire, singulières, un singleton est fondamentalement un moyen facile et correctce. (Même dans ce cas, l’utilisation de propriétés (lazy) est bénéfique, vos contrôleurs de vue n’ont donc pas besoin de savoir s’il s’agit d’un singleton ou non et vous pouvez toujours le stub/simulacre individuellement (c'est-à-dire pas seulement l'instance globale).)

6
Gero

Ce sont vos exigences telles que je les comprends:

  1. VC4 et VC8 doivent pouvoir partager l’état via une classe UserService.
  2. UserService ne doit pas être un singleton.
  3. UserService doit être fourni à VC4 et VC8 par injection de dépendance.
  4. Un cadre d'injection de dépendance ne doit pas être utilisé.

Dans ces limites, je suggérerais l'approche suivante.

Définissez une UserServiceProtocol qui possède des méthodes et/ou des propriétés pour accéder à et mettre à jour l'état. Par exemple:

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

Définissez une classe UserService qui implémente le protocole et stocke son état quelque part. 

Si l'état n'a besoin que de durer aussi longtemps que l'application est en cours d'exécution, vous pouvez stocker l'état dans une instance particulière de instance , mais cette instance devra être partagée entre VC4 et VC8. 

Dans ce cas, je vous recommanderais de créer et de conserver l'instance dans AppDelegate et de la transmettre à travers la chaîne de VC. 

Si l'état doit persister entre les lancements de l'application ou si vous ne souhaitez pas laisser une instance via la chaîne de VC, vous pouvez stocker l'état dans les valeurs par défaut de l'utilisateur, Core Data, Realm ou dans un nombre quelconque d'emplacements extérieurs à la classe elle-même.

Dans ce cas, vous pouvez créer la UserService dans VC3 et VC7 et la transmettre à VC4 et VC8. VC4 et VC8 auraient var userService: UserServiceProtocol?. La UserService devra restaurer son état à partir de la source externe. Ainsi, même si VC4 et VC8 ont des instances différentes de l’objet, l’état serait le même.

3
Mike Taverne

Tout d’abord, je pense que votre question repose sur une hypothèse erronée.

Vous définissez votre hiérarchie VC'c comme telle:

Exemple:

VC1 -> VC2 -> VC3 -> VC4

VC5 -> VC6 -> VC7 -> VC8

Toutefois, sur iOS (à moins que vous utilisiez des méthodes très étranges), il y aura toujours un parent commun, tel qu'un contrôleur de navigation, un contrôleur de barre de tabulation, un contrôleur maître-détail ou un contrôleur d'affichage de page.

Donc, je suppose qu'un schéma correct pourrait ressembler, par exemple, à ceci:

Onglet Contrôleur 1 -> Contrôleur de navigation 1 -> VC1 -> VC2 -> VC3 -> VC4

Onglet Contrôleur 1 -> Contrôleur de navigation 2 -> VC5 -> VC6 -> VC7 -> VC8

Je pense que le regarder comme ça facilite la réponse à votre question.

Maintenant, si vous demandez un avis sur la meilleure façon de gérer DI sur iOS, je dirais que rien n’existe. Toutefois, j’aime personnellement respecter la règle voulant que les objets ne soient pas responsables de leur propre création/initialisation. Alors des choses comme

private lazy var service: SomeService = SomeService()

sont hors de question. Je préférerais un init qui nécessite une instance SomeService ou au moins (facile pour ViewControllers):

var service: SomeService!

De cette façon, vous confiez au créateur de l’instance la responsabilité de rechercher les modèles/services appropriés, etc. (par exemple en utilisant la force de déballage), ce qui est en fait bon lors du développement).

Maintenant, comment allez-vous chercher ces modèles - est-ce en les initialisant, en les faisant circuler, en ayant un singleton, en utilisant des fournisseurs, des conteneurs, des coordinateurs, etc.? , quels que soient les outils que vous utilisez - de manière générale, tout fonctionne, si vous vous en tenez aux bonnes pratiques OOP.

2
user3581248

Voici une approche que j'ai utilisée sur quelques projets qui pourraient vous aider.

  1. Créez tous vos contrôleurs de vue via des méthodes d'usine dans un ViewControllerFactory.
  2. ViewControllerFactory a son propre objet UserService.
  3. Passez l'objet UserService de ViewControllerFactory aux contrôleurs de vue qui en ont besoin.

Un exemple modeste ici:

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

L'objet ViewControllerFactory peut être instancié et stocké dans AppDelegate.

C'est l'essentiel. De plus, je regarderais aussi ce qui suit (voir aussi les autres réponses qui ont fait de bonnes suggestions ici):

  1. Créez un protocole UserServiceProtocol auquel UserService se conforme. Cela facilite la création d'objets fictifs à des fins de test.
  2. Examinez le modèle de coordinateur pour gérer la logique de navigation.
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

Je trouve que le modèle de conception coordinateur/routeur est le mieux adapté pour injecter des dépendances et gérer la navigation dans les applications. Jetez un coup d'œil à ce billet, ça m'a beaucoup aidé https://medium.com/@dkw5877/flow-coordinators-333ed64f3dd

0
andrei

J'ai essayé de résoudre ce problème et téléchargé un exemple d'architecture ici: https://github.com/ivanovi/DI-demo

Pour que ce soit plus clair, j’ai simplifié la mise en œuvre à l’aide de trois VC, mais la solution fonctionnera avec n’importe quelle profondeur. La chaîne de contrôleurs de vue est la suivante:

Master -> Détail -> Plus de détails (où la dépendance est injectée)

L'architecture proposée repose sur quatre blocs de construction:

  • Dépôt de coordinateur: contient tous les coordinateurs et les états partagés. Injecte les dépendances requises.

  • ViewController Coordinator: Exécute la navigation vers le prochain ViewController. Le coordinateur détient une usine qui produit l'instance suivante nécessaire d'un VC. 

  • ViewController factory: responsable de l’initialisation et de la configuration d’un ViewController spécifique. Il appartient normalement à un coordinateur et est injecté par le référentiel de coordinateur dans le coordinateur.

  • Le ViewController: Le ViewController doit être présenté à l'écran.

NB: Dans l'exemple, je retourne l'instance VC nouvellement créée uniquement pour produire l'exemple - c'est-à-dire, dans la mise en œuvre réelle, renvoyer le VC n'est pas nécessaire.

J'espère que ça aide.

0
Ivan S Ivanov