hmlongco / Resolver

Swift Ultralight Dependency Injection / Service Locator framework
MIT License
2.14k stars 187 forks source link

Multi injection support #132

Closed iMattin closed 2 years ago

iMattin commented 2 years ago

Introduction

After working with this library for a while, I noticed a shortcoming in it: Multi Injection Suppose we need an injectable array of a specific type, If this dependency is already registered somewhere as an array once, everything will work fine. But what if we want the elements of this array to be registered in different parts of the code? 

Explain the scenario

In this scenario we have an injectable array of interceptors in the NetworkManager class which is defined in our network layer:

public protocol Interceptor {
  func intercept(_ request: Request) -> Response
}

public class NetworkManager {
 public var interceptors: [Interceptor]
}

We need to register various interceptors in different modules and we expect these interceptors to be injected as an array to our NetworkManager

Resolver.register(multi: true) { SetAuthHeaderInterceptor() as Interceptor }
Resolver.register(multi: true) { ActivityIndicatorInterceptor() as Interceptor }.scope(.application)

public class NetworkManager {
 @MultiInjected public var interceptors: [Interceptor]
}

In such a case we should tell the DI to register our Service with a different strategy.

Let me continue in the code...

ZsoltMolnarMBH commented 2 years ago

Hello!

I don't think this needs a dedicated feature in the library.

// Core (low level) module - begin
protocol Interceptor {
    func intercept()
}
// Core (low level) module - end

// Module A begin
class A_Interceptor: Interceptor {
    func intercept() {
        print("A")
    }
}
// Module A end

// Module B begin
class B_Interceptor: Interceptor {
    func intercept() {
        print("B")
    }
}
// Module B end

// Module C begin
class C_Interceptor: Interceptor {
    func intercept() {
        print("C")
    }
}
// Module C begin

// ... registration in the toplevel project - begin
        container.register([Interceptor].self) {
            [A.A_Interceptor(), B.B_Interceptor(), C.C_Interceptor()]
        }
// ... registration in the toplevel project - end

// USAGE
// In any module that has a dependency on "Core" module
class MyService {
    @Injected private var interceptors: [Interceptor]

    func intercept() {
        interceptors.forEach { $0.intercept() }
    }
}

Calling intercept() on a MyService instance prints the following as expected:

A
B
C

If you want distinguish multiple array registrations, you can use the name paramater.

I think your problem all comes down to correct project setup. You can have implementing classes in various modules (such as "A", "B" and "C"), and make a combined registration in the toplevel project (which is typically the application target itself).

remlostime commented 2 years ago

If for one protocol like NetworkServiceProtocol, we have different implementation like NetworkServiceA and NetworkServiceB. And in different use case, we want to inject different ones. How does Resolver framework fullfill this?

ZsoltMolnarMBH commented 2 years ago

An option is to use different resolution types where needed.

// Registration begin
        container.register { NetworkServiceA() }
            .implements(NetworkServiceProtocol.self)

        container.register { NetworkServiceB() }
            .implements(NetworkServiceProtocol.self)

        container.register(NetworkServiceProtocol].self) { container in
             return [container.resolve(NetworkServiceA.self), container.resolve(NetworkServiceB.self)]
        }
// Registration end

class MyService1 {
    // [NetworkServiceA instance, NetworkServiceB instance] resolved
    @Injected private var allServices: [NetworkServiceProtocol]    
}

class MyServiceA {
    // NetworkServiceA resolved
    @Injected private var aService: NetworkServiceA
}

class MyServiceB {
    // NetworkServiceB resolved
    @Injected private var bService: NetworkServiceB
}

Another way is to use services names.

hmlongco commented 2 years ago

I think I need to reject this, for a couple of reasons.

First and foremost is that it's relatively easy to accomplish without changing Resolver. Given...

protocol Interceptor {
    func intecept()
}

class MyInterceptorA: Interceptor {
    func intecept() {}
}

class MyInterceptorB: Interceptor {
    func intecept() {}
}

You can do....

extension Resolver {
    static func registerInterceptors1() {
        register { MyInterceptorA() }
        register { MyInterceptorB() }
        register([Interceptor].self) {  [resolve(MyInterceptorA.self), resolve(MyInterceptorB.self)]  }
    }
}

Or, if you prefer, using name spaces....

extension Resolver.Name {
    static let iA = Self("iA")
    static let iB = Self("iB")
}

extension Resolver {
    static func registerInterceptors2() {
        register(name: .iA) { MyInterceptorA() as Interceptor }
        register(name: .iB) { MyInterceptorB() as Interceptor }
        register([Interceptor].self) {  [resolve(name: .iA), resolve(name: .iB)]  }
    }
}

Either way, it's resolved as usual.

class Demo {
    @Injected var interceptors: [Interceptor]
}

The second reason is that, as is, the code only works correctly with a single container. It doesn't check every child container and see what Interceptors have of the same type been registered in each, nor would this approach honor mock containers for testing.

The final reason is that I've done this sort of thing before and the basic problem is that one usually needs their interceptors in a specific order (maybe retry first, then error handling, then logging, etc.), and that would be almost impossible to accomplish if various modules each registered their own interceptors in their own order.

As shown, with...

        register([Interceptor].self) { [resolve(name: .iA), resolve(name: .iB)]  }

You can explicitly control the order in which they're resolved and returned in the array.

Appreciate the work and the fact that you're using Resolver.

hmlongco commented 2 years ago

This is a fun approach with names where anyone can add a bunch of named interceptors and the final resolution doesn't need to know the exact types...

extension Resolver {
    static var interceptors: [Resolver.Name] = []
    static func registerInterceptors3() {
        register(name: .iA) { MyInterceptorA() as Interceptor }
        interceptors.append(.iA)

        register(name: .iB) { MyInterceptorB() as Interceptor }
        interceptors.append(.iB)

        register([Interceptor].self) {
            Resolver.interceptors.map { resolve(name: $0) }
        }
    }
}
iMattin commented 2 years ago

Thanks for your replies. Your solution was complete and convincing.