Comment les trouver et les éliminer
Les systèmes d’injection de dépendance comme Usine peut aider à rendre votre code plus facile à écrire et à tester. Mais parfois, des problèmes surgissent, et l’un des problèmes les plus courants que nous rencontrons dans les systèmes DI concerne les dépendances circulaires.
Disons que A a besoin de B pour être construit, et B a besoin d’un C. Mais que se passe-t-il si C a besoin d’un A ? Examinez les définitions de classe suivantes.
class CircularA {
@Injected(Container.circularB) var circularB
}class CircularB {
@Injected(Container.circularC) var circularC
}
class CircularC {
@Injected(Container.circularA) var circularA
}
Essayer de construire une instance de CircularA
va aboutir à une boucle infinie.
Pourquoi? Eh bien, le wrapper de propriété injecté de A a besoin d’un B pour construire un A. D’accord, très bien. Faisons-en un. Mais l’emballage de B a besoin d’un C, qui ne peut être fait sans injecter un A, qui encore une fois a besoin d’un B… et ainsi de suite. À l’infini.
Il s’agit d’une chaîne de dépendance circulaire.
Malheureusement, au moment où ce code est compilé et exécuté, il est trop tard pour briser le cycle. Nous avons effectivement codé une boucle infinie dans notre programme.
Ce qui nous amène à deux questions :
- Comment savoir ce qui ne va pas ?
- Comment pouvons-nous résoudre ce problème?
Commençons par trouver le problème. Bien qu’il soit facile d’examiner le code ci-dessus et de voir ce qui ne va pas, dans les grands systèmes avec des centaines voire des milliers de dépendances, trouver la chaîne de dépendance problématique peut être intimidant.
Comme mentionné, au moment où ce code a été compilé et exécuté, il est trop tard pour faire quoi que ce soit à ce sujet. Swift nous demande de fournir une instance d’un objet que nous pourrais créer une fois que nous avons toutes ses dépendances… mais les initialiseurs de propriété de cet objet exigent que nous fournissions d’abord une instance de une autre objet, qui nous emmène dans la chaîne. Laver rinser. Répéter.
Tout ce que l’application peut faire à ce stade, c’est mourir.
Mais avec Factory, nous pouvons mourir gracieusement et, ce faisant, vider la chaîne de dépendances qui montre où se situe le problème.
2022-12-23 14:57:23.512032-0600 FactoryDemo[47546:6946786] Factory/Factory.swift:393:
Fatal error: circular dependency chain - CircularA > CircularB > CircularC > CircularA
Lors de l’exécution en mode DEBUG, Factory suit désormais la chaîne de dépendance et émet une erreur fatale lorsqu’une chaîne de dépendance circulaire est détectée. Nous n’avons pas besoin de comprendre ce qui ne va pas en examinant la pile d’appels. L’usine nous le dira.
CircularA > CircularB > CircularC > CircularA
Nous avons essayé de faire un A, qui fait un B, qui fait un C… qui revient en boucle et essaie à nouveau de faire un A.
Avec les informations ci-dessus en main, nous devrions être en mesure de trouver le problème et de le résoudre.
Nous pourrions demander à l’usine de résoudre le problème elle-même. Considérez le changement suivant pour CircularC
et une nouvelle définition d’usine pour circularA
.
class CircularC {
weak var circularA: CircularA?
}extension Container {
static var circularA = Factory<CircularA> {
let a = CircularA()
a.circularB.circularC.circularA = a
return a
}
}
Ce mécanisme est généralement utilisé pour fixer des relations parent-enfant strictes et peut être utilisé ici… mais c’est assez malodorant. Et que se passe-t-il si quelqu’un essaie de faire un CircularB
par eux-même?
Voyons si nous pouvons faire mieux.
Chargement paresseux
Une solution classique au problème réside dans le chargement différé.
Changez simplement le wrapper d’injection de CircularC en LazyInjected
ou, mieux encore, WeakLazyInjected
afin d’éviter un cycle de rétention.
class CircularC {
@WeakLazyInjected(Container.optionalA) var circularA
}
Depuis cet exemple de CircularA
ne sera pas fourni tant qu’il n’aura pas été demandé, après l’objet a été construit, la chaîne de dépendance circulaire a été rompue.
Notez que si nous voulons réellement que C ait une référence au A d’origine, nous allons probablement modifier la portée d’usine d’origine pour circularA
à .shared
et pointe optionalA
à cette usine.
Quoi qu’il en soit, ce n’est toujours pas optimal.
Refactorisation
D’un point de vue architectural, si A dépend de B, nous voulons généralement que A puisse voir B, mais B ne devrait même pas savoir que A existe.
La visibilité coule le long de la chaîne.
En tant que tel, une meilleure solution impliquerait probablement de trouver et de casser la fonctionnalité qui CircularA
et CircularC
dépendent dans un troisième objet qu’ils pourraient tous deux inclure.
Les dépendances circulaires telles que celle-ci constituent généralement une violation du principe de responsabilité unique et doivent être évitées en premier lieu.
Factory effectue donc une détection de chaîne de dépendance circulaire, mais comment le fait-il ?
Au plus profond de Factory, il existe une seule fonction dans laquelle une dépendance spécifique est résolue. Il vérifie si une usine a été remplacée par un nouvel enregistrement, et il vérifie également qu’un objet est mis en cache dans l’une des différentes portées.
func resolve(_ params: P) -> T {
let _ = Container.autoRegistrationCheck
let currentFactory: (P) -> T = (SharedContainer.Registrations.factory(for: id) as? TypedFactory<P, T>)?.factory ?? factory
let instance: T = scope?.resolve(id: id, factory: { currentFactory(params) }) ?? currentFactory(params)
SharedContainer.Decorator.decorate?(instance)
return instance
}
Tout coule à travers ce seul endroit.
Afin de fournir une détection de dépendance circulaire, ce code s’est développé et ressemble maintenant à ceci.
func resolve(_ params: P) -> T {
let _ = Container.autoRegistrationChecklet currentFactory: (P) -> T = (SharedContainer.Registrations.factory(for: id) as? TypedFactory<P, T>)?.factory ?? factory
#if DEBUG
defer { dependencyChain.removeLast(); dependencyLock.unlock() }
dependencyLock.lock()
let typeComponents = String(describing: T.self).components(separatedBy: CharacterSet(charactersIn: "<>"))
let typeName = typeComponents.count > 1 ? typeComponents[1] : typeComponents[0]
let typeIndex = dependencyChain.firstIndex(where: { $0 == typeName })
dependencyChain.append(typeName)
if let index = typeIndex {
fatalError("circular dependency chain - \(dependencyChain[index...].joined(separator: " > "))")
}
#endif
let instance: T = scope?.resolve(id: id, factory: { currentFactory(params) }) ?? currentFactory(params)
SharedContainer.Decorator.decorate?(instance)
return instance
}
Tout le code original est là, mais il y a maintenant un DEBUG
section qui suit le nom de la classe actuelle en le poussant sur une pile (tableau), en créant l’instance souhaitée, puis en supprimant le nom sur la pile.
Cette fonction est réentrante, donc si tout en créant une instance de A, elle essaie de créer un B, elle appellera à nouveau cette fonction. Idem pour le cas où B essaie de faire un C, et encore une fois quand C essaie de faire un A.
Mais chaque fois que nous voulons créer une nouvelle instance d’un type, nous vérifions si ce type est déjà sur la pile.
Et si c’est le cas, alors nous avons une chaîne de dépendance circulaire et nous devons abandonner, vidant la chaîne dans le processus.
Un petit point est qu’il est possible qu’un type Factory soit facultatif, il y a donc un peu de code supplémentaire impliqué pour extraire le nom du type de base d’une chaîne de type qui ressemble à « Optional
Et puisque Factory est thread-safe, nous effectuons un verrouillage supplémentaire autour de notre code de détection pour empêcher la corruption de notre tableau.
Factory continue d’évoluer, de grandir et de s’améliorer grâce au soutien de la communauté. J’apprécie l’aide, les commentaires et oui, même les rapports de bogues.
Vous avez quelque chose à dire ? Comme toujours, laissez un commentaire ou un Like ci-dessous.