Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi injection support #132

Closed
wants to merge 9 commits into from
Closed

Multi injection support #132

wants to merge 9 commits into from

Conversation

iMattin
Copy link

@iMattin iMattin commented Oct 7, 2021

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...

Mattin Abdollahi added 8 commits October 6, 2021 22:04
Remove duplicate codes in static resolver methods
A container of a collection of ResolverRegistration<Service>
Storing ResolverRegistration<Service> with a different strategy in to the dictionaries
multiResolve will return an array of a Service which are registered using multi = true
@iMattin iMattin changed the title Multi Injection Multi injection support Oct 8, 2021
@ZsoltMolnarMBH
Copy link
Contributor

ZsoltMolnarMBH commented Oct 11, 2021

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
Copy link

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
Copy link
Contributor

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
Copy link
Owner

hmlongco commented Oct 11, 2021

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
Copy link
Owner

hmlongco commented Oct 12, 2021

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
Copy link
Author

iMattin commented Oct 12, 2021

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

@iMattin iMattin closed this Oct 12, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants