Oliver Pearmain
Oliver Pearmain

Reputation: 20610

Protocol Composition for Dependency Injection - Causality Dilemma / Compile Issue

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

Answers (2)

Oliver Pearmain
Oliver Pearmain

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

Michael Voong
Michael Voong

Reputation: 488

You could use a private(set) lazy var:

private(set) lazy var networking: NetworkingProtocol = {
    return Networking(dependencies: self)
}()

Upvotes: 1

Related Questions