square / Cleanse

Lightweight Swift Dependency Injection Framework
Other
1.78k stars 90 forks source link

Reuse an instance for multiple protocol bindings #183

Closed nwolvo closed 1 year ago

nwolvo commented 1 year ago

I'm new to Cleanse and can't figure out how to reuse an instance in a graph for multiple protocol conformances.

Say we have a FeatureModule that tries to bind a FeatureRouterImpl that conforms to a Router protocol and a FeatureInternalRouter protocol. In the sample below two separate instances will be created.

In Swinject, for example, it is solved with type forwarding (https://github.com/Swinject/Swinject/blob/master/Documentation/TypeForwarding.md).

public struct FeatureModule: Cleanse.Module {

    public static func configure(binder: Binder<Singleton>) {
        binder
            .bind(Router.self)
            .sharedInScope()
            .to(factory: FeatureRouterImpl.init)

        binder
            .bind(FeatureInternalRouter.self)
            .sharedInScope()
            .to(factory: FeatureRouterImpl.init)
      }
}

How can I solve this so that only one instance for both protocols is created?

Another question is how the same instance can be provided in a Router collection? This is a requirement of another type (MainRouter) through Constructor Injection.

class MainRouter {
    private var routers: [Router] = []

    init(routers: [Router]) {
        self.routers = routers
    }
}

public struct FeatureModule: Cleanse.Module {
    public static func configure(binder: Binder<Singleton>) {
        binder
            .bind(Router.self)
            .intoCollection()
            .to(factory: FeatureRouterImpl.init)
    }
}

Thanks already in advance!

ElleSkye117 commented 1 year ago

For the first part, I do something similar in one of my projects.

You need to make sure you only create the implementation once, your code does it once per Router and once per FeatureInternalRouter types. Instead, create the implementation once via sharedInScope() separately and then in the factory function for the two protocols get that implementation and return "as" the protocol type. see example

public struct FeatureModule: Cleanse.Module {
    public static func configure(binder: Binder<Singleton>) {
       // create the underlying implementation once for the graph
        binder
           .bind(FeatureRouterImpl.self)
           .sharedInScope()
           .to(factory: FeatureRouterImpl.init)
       // Assumes FeatureRouterImpl implements both Router and FeatureInternalRouter protocols
        binder
            .bind(Router.self)
            .to { $0 as FeatureRouterImpl }
        binder
            .bind(FeatureInternalRouter.self)
            .to { $0 as FeatureRouterImpl }
      }
}

for your second questions, I think you need to make the factory function return an array. You need to specify what actual Router elements go in that collection. Basically, your to(factory: FeatureRouterImpl.init) needs to be to(factory: SomeFunctionThatReturns [Router] type)

Im not sure if there is a more concise way to do the below example, but its how I do it on another project.

example

public struct FeatureModule: Cleanse.Module {
    public static func configure(binder: Binder<Singleton>) {
        binder
            .bind(Router.self)
            .intoCollection()
           // pull dependencies you might need to build the elements in the array out of the closure arguments based on types
            .to { (arg1: int) in
                  [RouterImplOne(intArg: arg1), RouterImplTwo(), ...] 
            }
     }
}
nwolvo commented 1 year ago

@ElleSkye117 thanks for the detailed explanation and examples. This is exactly what I was looking for. I didn't know that you can also bind types without using the factory call.

For the second point, I decided to list the specific routers individually in the Constructor and not use a collection.

Thank you :)