// // DIContainer.swift // // // Created by Michal Fousek on 01.05.2023. // import Foundation /// This class represents depedency injection containers. class DIContainer { /// Structure that represents one registered dependency. struct Dependency { /// Closure which creates instance of the dependency. let factory: (DIContainer) -> Any /// Indicates if dependency is singleton. If this is `true` then `DIContainer` creates only one instance of this dependency and returns /// it every time `resolve()` for the dependency is called. let isSingleton: Bool /// If the dependency is singleton then instance is stored here. let instance: Any? } /// If this is `true` then `mockedDependencies` is used first to resolve dependencies. This is used for mocking in tests. var isTestEnvironment = false private let lock = NSRecursiveLock() /// Dependencies are stored here. private var dependencies: [String: Dependency] = [:] /// Mocked dependencies are stored here. private var mockedDependencies: [String: Dependency] = [:] init() { } private func key(for type: T.Type) -> String { return String(describing: T.self) } func register(type: T.Type, isSingleton: Bool, factory: @escaping (DIContainer) -> T) { lock.lock() let key = self.key(for: type) let depedency = Dependency(factory: factory, isSingleton: isSingleton, instance: nil) dependencies[key] = depedency lock.unlock() } func mock(type: T.Type, isSingleton: Bool, factory: @escaping (DIContainer) -> T) { lock.lock() let key = self.key(for: type) let depedency = Dependency(factory: factory, isSingleton: isSingleton, instance: nil) mockedDependencies[key] = depedency lock.unlock() } func resolve(_ type: T.Type) -> T { lock.lock() defer { lock.unlock() } let key = self.key(for: type) let possibleDependency = (isTestEnvironment ? mockedDependencies[key] : dependencies[key]) ?? dependencies[key] guard let dependency = possibleDependency else { // When dependency is resolved before it's registered then the app crashes. It would be possible to not crash and throw some error here. // But it complicates the rest of the code. And maybe it doesn't make sense because this kind of error is not recoverable. It can be fixed // only by updating the code. fatalError("Doesn't have registered dependency for type \(type).") } let instance: Any if dependency.isSingleton, let singleton = dependency.instance { instance = singleton } else { instance = dependency.factory(self) } if dependency.isSingleton && dependency.instance == nil { dependencies[key] = Dependency(factory: dependency.factory, isSingleton: dependency.isSingleton, instance: instance) } guard let instance = instance as? T else { // When dependency is resolved but instance of created depedency is different than expected type the app crashes. It would be possible to // not crash and throw some error here. But it complicates the rest of the code. And maybe it doesn't make sense because this kind of // error is not recoverable. It can be fixed only by updating the code. fatalError("Getting dependency for type \(type) but created instance is \(instance)") } return instance } }