Reputation: 20610
I am trying to use Swift "Protocol Composition" for dependency injection for the first time. There are various blog posts from well respected engineers in the field advocating this approach but I am unable to get the code to compile once there are dependencies that depend upon other dependencies.
The problem is that until your main concrete AllDependencies
instance is initialised it cannot be used to initialise the child dependencies, but conversely you cannot create the child dependencies without the concrete AllDependencies
instance.
Chicken & egg. Rock and hard place.
I'll try and provide the simplest example I can...
protocol HasAppInfo {
var appInfo: AppInfoProtocol { get }
}
protocol AppInfoProtocol {
var apiKey: String { get }
}
struct AppInfo: AppInfoProtocol {
let apiKey: String
}
protocol HasNetworking {
var networking: NetworkingProtocol { get }
}
protocol NetworkingProtocol {
func makeRequest()
}
class Networking: NetworkingProtocol {
typealias Dependencies = HasAppInfo
let dependencies: Dependencies
init(dependencies: Dependencies) {
self.dependencies = dependencies
}
func makeRequest() {
let apiKey = self.dependencies.appInfo.apiKey
// perform request sending API Key
// ...
}
}
class AllDependencies: HasAppInfo, HasNetworking {
let appInfo: AppInfoProtocol
let networking: NetworkingProtocol
init() {
self.appInfo = AppInfo(apiKey: "whatever")
/// **********************************************************
/// *** ERROR: Constant 'self.networking' used before being initialized
/// **********************************************************
self.networking = Networking(dependencies: self)
}
}
It seems like it might be possible to resolve this with use of lazy var
, {get set}
or mutating
dependencies but that seems extermely unsafe because any code in your system can mutate your dependencies at will.
Would appreciate understanding how others have resolved what seems like a pretty fundamental issue with this approach.
References
Upvotes: 1
Views: 310
Reputation: 20610
PS: I posted the original question. I have already accepted the lazy var
answer, this is the simplest way forward, however I wanted to post this alternative solution that uses a generic DependencyFactory
in case it helps others.
protocol Dependencies:
HasAppInfo &
HasNetworking
{}
class DependencyFactory {
typealias Factory<T> = (Dependencies) -> T
private enum DependencyState<T> {
case registered(Factory<T>)
case initialised(T)
}
private var dependencyStates = [String: Any]()
func register<T>(_ type: T.Type, factory: @escaping Factory<T>) {
dependencyStates[key(for: type)] = DependencyState<T>.registered(factory)
}
func unregister<T>(_ type: T.Type) {
dependencyStates[key(for: type)] = nil
}
func resolve<T>(_ type: T.Type, dependencies: Dependencies) -> T {
let key = self.key(for: type)
guard let dependencyState = dependencyStates[key] as? DependencyState<T> else {
fatalError("Attempt to access unregistered `\(type)` dependency")
}
switch dependencyState {
case let .registered(factoryClosure):
let dependency = factoryClosure(dependencies)
dependencyStates[key] = DependencyState<T>.initialised(dependency)
return dependency
case let .initialised(dependency):
return dependency
}
}
private func key<T>(for type: T.Type) -> String {
return String(reflecting: type)
}
}
class AllDependencies: Dependencies {
private let dependencyFactory = DependencyFactory()
init() {
dependencyFactory.register(AppInfoProtocol.self, factory: { dependencies in
return AppInfo(apiKey: "whatever")
})
dependencyFactory.register(NetworkingProtocol.self, factory: { dependencies in
return Networking(dependencies: dependencies)
})
}
var appInfo: AppInfo {
return dependencyFactory.resolve(AppInfoProtocol.self, dependencies: self)
}
var networking: Networking {
return dependencyFactory.resolve(NetworkingProtocol.self, dependencies: self)
}
}
Upvotes: 0
Reputation: 488
You could use a private(set) lazy var:
private(set) lazy var networking: NetworkingProtocol = {
return Networking(dependencies: self)
}()
Upvotes: 1