Malgré le fait que le modèle existe depuis plus d'une décennie, il existe de nombreux articles (et traductions), néanmoins, il y a de plus en plus de différends, de commentaires, de questions et de diverses réalisations.
Il y a suffisamment d'informations même sur le hub, mais le fait que l'on discute partout
comment le faire, mais pratiquement nulle part -
POURQUOI, m'a inspiré pour écrire le message. Est-il possible de créer une bonne architecture si vous ne savez pas à quoi elle sert et dans quoi elle devrait être bonne? Certains principes et tendances claires peuvent être pris en compte - cela aidera à minimiser les problèmes imprévus, mais la compréhension est encore meilleure.
L'injection de dépendance est un modèle de conception dans lequel les champs ou les paramètres de création d'un objet sont configurés en externe.
Sachant que beaucoup se limiteront à la lecture des premiers paragraphes, j'ai changé l'article.
Malgré le fait qu'une telle «définition» de DI se trouve dans de nombreuses sources - elle est ambiguë, car elle fait penser à l'utilisateur que l'injection est quelque chose qui remplace la création / initialisation d'objets, ou du moins est très activement impliqué dans ce processus. Bien sûr, personne n'interdira de faire une telle implémentation de DI. Mais DI peut être un wrapper passif autour de la création d'un objet qui fournit la fourniture de paramètres d'entrée. Dans une telle implémentation, on obtient un autre niveau d'abstraction et une excellente séparation des tâches: l'objet lui-même est responsable de son initialisation, et l'injection implémente le stockage des données et leur fournit des modules d'application.
Maintenant, tout est en ordre et en détail.
Je vais commencer par un simple, pourquoi il y avait un besoin de nouveaux modèles, et pourquoi certains anciens modèles sont devenus de portée très limitée?
À mon avis, l'essentiel des changements ont été introduits par l'introduction massive d'auto-tests. Et pour ceux qui écrivent activement des autotests, cet article est évident comme un jour blanc, vous ne pouvez pas lire plus loin. Vous seul ne pouvez pas imaginer combien de personnes ne les écrivent pas. Je comprends que les petites entreprises et les start-ups ne disposent pas de ces ressources, mais, malheureusement, les grandes entreprises ont souvent des problèmes plus prioritaires.
Le raisonnement ici est très simple. Supposons que vous testiez une fonction avec les paramètres
a et
b et que vous vous attendiez à obtenir le résultat
x . À un moment donné, vos attentes ne se réalisent pas, la fonction renvoie le résultat
y et après avoir passé un certain temps, vous trouvez un singleton à l'intérieur de la fonction, ce qui, dans certains états, amène le résultat de la fonction à une valeur différente. Ce singleton était appelé une
dépendance implicite et refusait de toutes les manières possibles de l'utiliser dans de telles situations. Malheureusement, vous ne jeterez pas de mots de la chanson, sinon ce sera une chanson complètement différente. Par conséquent, nous retirons notre singleton comme variable d'entrée dans la fonction. Nous avons maintenant 3 variables d'entrée
a ,
b ,
s . Tout semble évident: nous modifions les paramètres - nous obtenons un résultat sans ambiguïté.
Bien que je ne donne pas d'exemples. De plus, nous ne parlons pas seulement des fonctions au sein d'une classe, c'est un argument schématique qui peut également être appliqué à la création d'une classe, d'un module, etc.
Notes de singletonRemarque 1. Si, étant donné la critique du modèle singleton, vous décidez de le remplacer, par exemple, par UserDefaults, alors par rapport à cette situation, la même dépendance implicite se profile.
Note 2. Il n'est pas tout à fait correct de dire que ce n'est qu'à cause de l'autotest qu'il n'est pas utile d'utiliser des singletones à l'intérieur du corps de la fonction. En général, du point de vue de la programmation, il n'est pas tout à fait correct qu'avec la même entrée, la fonction produise des résultats différents. C'est juste que dans les autotests, ce problème apparaissait plus clairement.
Complétez l'exemple ci-dessus. Vous avez un objet qui contient 9 paramètres utilisateur (variables), par exemple, le droit de lire / modifier / signer / imprimer / transférer / supprimer / verrouiller / exécuter / copier un document. Votre fonction utilise uniquement trois variables de ces paramètres. Que passez-vous à la fonction: l'objet entier avec 9 variables comme un paramètre, ou seulement trois réglages nécessaires avec trois paramètres distincts? Très souvent, nous agrandissons les objets transférés afin de ne pas définir de nombreux paramètres, c'est-à-dire que nous sélectionnons la première option. Cette méthode sera considérée comme le transfert de
"dépendances déraisonnablement larges" . Comme vous l'avez déjà deviné, aux fins de l'auto-test, il est préférable d'utiliser la deuxième option et de ne transmettre que les paramètres utilisés.
Nous avons tiré 2 conclusions:
- la fonction doit recevoir tous les paramètres nécessaires à l'entrée
- la fonction ne doit pas recevoir de paramètres d'entrée inutiles
Nous voulions le meilleur - mais avons obtenu une fonction avec 6 paramètres. Supposons que tout soit en ordre à l'intérieur de la fonction, mais que quelqu'un devrait prendre la responsabilité de fournir des paramètres d'entrée à la fonction. Comme je l'ai déjà écrit, mon raisonnement est sommaire. Je veux dire non seulement une fonction de classe ordinaire, mais plutôt une fonction d'initialisation / création de module (vip, viper, objet de données, etc.). Dans ce contexte, nous reformulons la question: qui doit fournir les paramètres d'entrée pour créer le module?
Une solution serait de déplacer ce cas vers le module appelant. Mais il s'avère que le module appelant doit passer les paramètres de l'enfant. Cela entraîne les complications suivantes:
Premièrement, un peu plus tôt, nous avons décidé d'éviter les "dépendances excessivement larges". Deuxièmement, vous n'avez pas à travailler dur pour comprendre qu'il y aura beaucoup de paramètres, et il sera très fastidieux de les modifier chaque fois que vous ajouterez des modules enfants, cela fait même mal de penser à supprimer des modules enfants. Soit dit en passant, dans certaines applications, il est impossible de créer une hiérarchie de modules: regardez n'importe quel réseau social: profil -> amis -> profil de l'ami -> amis de l'ami, etc. Troisièmement, le principe SOLI
D peut être rappelé sur ce sujet: «Les modules de niveau supérieur sont indépendants des modules de niveau inférieur»
D'où l'idée de faire de la création / initialisation du module une structure distincte. Ensuite, il est temps d'écrire quelques lignes comme exemple:
class AccountList { public func showAccountDetail(account: String) { let accountDetail = AccountDetail.make(account: account)
Dans l'exemple, il existe un module de la liste des comptes AccountList, qui appelle le module d'informations détaillées sur le compte AccountDetail.
Pour initialiser le module AccountDetail, 3 variables sont nécessaires. La variable Account AccountDetail reçoit du module parent, les variables permission1, permission2 sont injectées. En raison de l'injection, un appel de module avec les détails de la facture ressemblera à:
let accountDetail = AccountDetail.make(account: account)
au lieu de
let accountDetail = AccountDetail(account: account, permission1: p1, permission2: p2)
et le module parent de la liste des comptes, AccountList, sera déchargé de l'obligation de transmettre des paramètres avec des autorisations dont il ne sait rien.
J'ai rendu l'implémentation d'injection (assemblage) dans une fonction statique dans une extension de classe. Mais la mise en œuvre peut être à votre discrétion.
Comme nous le voyons:
- Le module a reçu les paramètres nécessaires. Sa création et son exécution peuvent être testées en toute sécurité sur tous les ensembles de valeurs.
- Les modules sont indépendants, il n'y a pas besoin de transférer quoi que ce soit pour les enfants ou juste le minimum nécessaire.
- Les modules NE font PAS le travail de fournir des données, ils utilisent des données toutes faites (p1, p2). Ainsi, si vous souhaitez modifier quelque chose dans le stockage ou la fourniture de données, vous n'avez pas à modifier le code fonctionnel des modules (ainsi que leurs autotests), mais vous devez uniquement modifier le système d'assemblage lui-même ou des extensions avec l'assemblage.
L'essence de l'injection de dépendances est la construction d'un tel processus dans lequel, lors de l'appel d'un module à partir d'un autre, un objet / mécanisme indépendant transfère (injecte) des données au module appelé. En d'autres termes, le module appelé est configuré en externe.
Il existe plusieurs méthodes de configuration:
Injection constructeur ,
injection propriété, injection interface .
Pour Swift:
Injection d'initialisation, injection de propriété, injection de méthode .
Les injections et propriétés des constructeurs (initialisation) sont les plus courantes.
Important: dans presque toutes les sources, il est recommandé de privilégier les injections de constructeur. Comparer l'injection de constructeur / initialiseur et l'injection de propriété:
let account = .. let p1 = ... let p2 = ... let accountDetail = AccountDetail(account: account, permission1: p1, permission2: p2)
mieux que
let accountDetail = AccountDetail() accountDetail.account = .. accountDetail.permission1 = ... accountDetail.permission2 = ...
Il semble que les avantages de la première méthode soient évidents, mais pour une raison quelconque, certains comprennent l'injection comme la configuration d'un objet déjà créé et utilisent la deuxième méthode. Je suis pour la première méthode:
- la création par le designer garantit un objet valide;
- avec l'injection de propriété, il n'est pas clair s'il est nécessaire de tester une modification d'une propriété dans des endroits autres que la création;
- dans les langues qui utilisent l'optionnalité, pour implémenter l'injection de propriété, vous devez rendre les champs facultatifs ou trouver des méthodes d'initialisation intelligentes (les paresseuses ne fonctionneront pas toujours). L'option facultative excessive ajoute du code inutile et des suites de tests inutiles.
Cependant, jusqu'à ce que nous nous débarrassions de certaines dépendances, nous les avons simplement déplacées d'une épaule à l'autre. Une question logique est de savoir où obtenir les données de l'assemblage lui-même (make fonction dans l'exemple).
L'utilisation de singletones dans le mécanisme d'assemblage ne conduit plus aux problèmes ci-dessus de dépendance cachée, car Vous pouvez tester la création de modules avec n'importe quel ensemble de données.
Mais ici, nous sommes confrontés à un autre inconvénient du singleton: une mauvaise manipulation (vous pouvez probablement apporter beaucoup d'arguments haineux, mais la paresse). Il n'est pas bon de disperser vos nombreux singletones stockés dans des assemblages, par analogie avec n'importe qui, car ils étaient dispersés dans des modules fonctionnels. Mais même un tel refactoring sera déjà la première étape vers l'hygiène, car alors vous pouvez restaurer l'ordre dans les assemblages presque sans affecter les tests de code et de module.
Si vous souhaitez rationaliser davantage l'architecture, ainsi que les transitions de test et les travaux d'assemblage, vous devrez travailler un peu plus.
Le concept DI nous propose de stocker toutes les données nécessaires dans un conteneur. C'est pratique. Premièrement, l'enregistrement (l'enregistrement) et la réception (la résolution) des données passent par un objet conteneur; en conséquence, il est plus facile de gérer les données et de les tester. Deuxièmement, vous pouvez prendre en compte la dépendance des données les unes par rapport aux autres. Dans de nombreuses langues, y compris swift, il existe des conteneurs de gestion des dépendances prêts à l'emploi, généralement les dépendances forment un arbre. Les avantages et les inconvénients restants que je ne vais pas énumérer, vous pouvez les lire sur les liens que j'ai postés au début de l'article.
Voici à quoi pourrait ressembler l'assemblage utilisant le conteneur.
import Foundation import Swinject public class Configurator { private static let container = Container() public static func register<T>(name: String, value: T) { container.register(type(of: value), name: name) { _ in value } } public static func resolve<T>(service: T.Type, name: String) -> T? { return container.resolve(service, name: name) } } extension AccountDetail { public static func make(account: String) -> AccountDetail? { if let p1 = Configurator.resolve(service: Bool.self, name: "permission1"), let p2 = Configurator.resolve(service: Bool.self, name: "permission2") { return AccountDetail(account: account, permission1: p1, permission2: p2) } else { return nil } } }
Il s'agit d'un exemple d'implémentation possible. L'exemple utilise le framework
Swinject , né il n'y a pas si longtemps. Swinject vous permet de créer un conteneur pour la gestion automatisée des dépendances et vous permet également de créer des conteneurs pour les storyboards. Plus d'informations sur Swinject peuvent être trouvées dans les exemples sur
raywenderlich . J'aime vraiment ce site, mais cet exemple n'est pas le plus réussi, car il considère l'utilisation du conteneur uniquement dans les autotests, tandis que le conteneur doit être posé dans l'architecture de l'application. Vous dans votre code, vous pouvez écrire un conteneur vous-même.
Merci à tous pour cela. J'espère que vous ne vous êtes pas ennuyé en lisant ce texte.