Как их найти и устранить

Системы внедрения зависимостей, такие как Factory, могут помочь упростить написание и тестирование вашего кода. Но иногда возникают проблемы, и одна из наиболее распространенных проблем, которые мы наблюдаем в системах DI, связана с циклическими зависимостями.
Что такое циклическая зависимость?
Предположим, что A нуждается в B для создания, а B нуждается в C. Но что происходит, если C нуждается в A? Изучите следующие определения классов.
class CircularA {
@Injected(Container.circularB) var circularB
}
class CircularB {
@Injected(Container.circularC) var circularC
}
class CircularC {
@Injected(Container.circularA) var circularA
}
Попытка построить экземпляр CircularA приведет к бесконечному циклу.
Почему? Что ж, обертка свойств, внедренных в A, нуждается в B для создания A. Хорошо, хорошо. Давайте сделаем один. Но обертка B нуждается в C, который не может быть сделан без внедрения A, которому снова нужен B… и так далее. До бесконечности.
Это циклическая цепочка зависимостей.
К сожалению, к тому времени, когда этот код скомпилирован и запущен, уже слишком поздно прерывать цикл. Мы фактически запрограммировали бесконечный цикл в нашу программу.
Что приводит нас к двум вопросам:
- Как мы узнаем, что не так?
- Как это исправить?
Как мы узнаем, что не так?
Начнем с поиска проблемы. В то время как приведенный выше код легко изучить и понять, что не так, в более крупных системах с сотнями или даже тысячами зависимостей поиск проблемной цепочки зависимостей может быть сложной задачей.
Как уже упоминалось, к тому времени, когда этот код будет скомпилирован и запущен, будет слишком поздно что-либо с этим делать. Swift требует, чтобы мы предоставили экземпляр объекта, который мы могли создать, когда у нас есть все его зависимости… но инициализаторы свойств этого объекта требуют, чтобы мы сначала предоставили экземпляр другого объект, который ведет нас вниз по цепочке. Стирать, полоскать. Повторить.
Все, что приложение может сделать в этот момент, это умереть.
Но с Factory мы можем изящно умереть, и в процессе сбросить цепочку зависимостей, которая покажет, в чем проблема.
2022-12-23 14:57:23.512032-0600 FactoryDemo[47546:6946786] Factory/Factory.swift:393: Fatal error: circular dependency chain - CircularA > CircularB > CircularC > CircularA
При работе в режиме DEBUG Factory теперь отслеживает цепочку зависимостей и выдает фатальную ошибку при обнаружении циклической цепочки зависимостей. Нам не нужно выяснять, что не так, исследуя стек вызовов. Фабрика расскажет.
CircularA > CircularB > CircularC > CircularA
Мы пытались сделать A, который создает B, который создает C… который зацикливается и снова пытается сделать A.
Имея вышеуказанную информацию, мы сможем найти проблему и устранить ее.
Устранение проблемы
Мы могли бы попросить фабрику решить проблему самостоятельно. Рассмотрим следующее изменение в CircularC и новое определение Factory для circularA.
class CircularC {
weak var circularA: CircularA?
}
extension Container {
static var circularA = Factory<CircularA> {
let a = CircularA()
a.circularB.circularC.circularA = a
return a
}
}
Этот механизм обычно используется для исправления строгих родительско-дочерних отношений и может быть использован здесь… но он довольно вонючий. А что произойдет, если кто-то попытается сделать CircularB самостоятельно?
Посмотрим, сможем ли мы сделать лучше.
Ленивая загрузка
Классическое решение проблемы заключается в ленивой загрузке.
Просто измените обертку инъекции CircularC на LazyInjected или, что еще лучше, на WeakLazyInjected, чтобы избежать цикла сохранения.
class CircularC {
@WeakLazyInjected(Container.optionalA) var circularA
}
Поскольку этот экземпляр CircularA не будет предоставлен до тех пор, пока он не будет запрошен, после создания объекта циклическая цепочка зависимостей разорвана.
Обратите внимание: если мы действительно хотим, чтобы C имел ссылку на исходный A, мы, вероятно, изменим исходную область Factory с circularA на .shared и укажем optionalA на эту фабрику.
Тем не менее, это все еще не оптимально.
Рефакторинг
С архитектурной точки зрения, если A зависит от B, то мы обычно хотим, чтобы A мог видеть B, но B не должен знать, что A вообще существует.
Видимость течет по цепочке.
Таким образом, лучшее решение, вероятно, повлечет за собой поиск и выделение функций, от которых зависят CircularA и CircularC, в третий объект, который они оба могли бы включить.
Циклические зависимости, подобные этой, обычно являются нарушением принципа единой ответственности, и их следует избегать в первую очередь.
Под капотом.
Итак, Factory выполняет обнаружение циклической цепочки зависимостей, но как она это делает?
Глубоко внутри Factory есть одна-единственная функция, в которой разрешается конкретная зависимость. Он проверяет, была ли фабрика переопределена новой регистрацией, а также проверяет, кэшируется ли объект в одной из различных областей.
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
}
Все течет через это одно место.
Чтобы обеспечить циклическое обнаружение зависимостей, этот код был расширен и теперь выглядит так.
func resolve(_ params: P) -> T {
let _ = Container.autoRegistrationCheck
let 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
}
Весь исходный код присутствует, но теперь есть раздел DEBUG, который отслеживает текущее имя класса, помещая его в стек (массив), создавая нужный экземпляр, а затем удаляя имя из стека.
Эта функция является реентерабельной, поэтому, если при создании экземпляра A она попытается создать B, она снова вызовет эту функцию. То же самое для случая, когда B пытается сделать C, и еще раз, когда C пытается сделать A.
Но каждый раз, когда мы хотим создать новый экземпляр типа, мы проверяем, находится ли этот тип уже в стеке.
И если это так, то у нас есть циклическая цепочка зависимостей, и нам нужно прервать ее, сбросив цепочку в процессе.
Небольшой момент заключается в том, что тип Factory может быть необязательным, поэтому требуется дополнительный код для извлечения имени базового типа из строки типа, которая выглядит как «Optional‹CircularA›».
А поскольку Factory является потокобезопасной, мы делаем дополнительную блокировку нашего кода обнаружения, чтобы предотвратить повреждение нашего массива.
Блок завершения
Factory все еще развивается, растет и становится лучше благодаря поддержке сообщества. Я ценю помощь, комментарии и, да, даже отчеты об ошибках.
Есть что сказать? Как всегда, оставьте комментарий или лайк ниже.
Эта статья является частью серии Swift Dependency Injection.