hmlongco / Resolver

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

How to handle multiple class conformance for Interface Injection #99

Closed erenkabakci closed 3 years ago

erenkabakci commented 3 years ago

As documentation suggests, interface injection is supported out of the box and frankly should be the default way (personal opinion). Since injecting abstractions via protocols should be preferred instead of concrete classes (for testability and composition purposes), using Resolver for such case works but only partially. Here is an example below.

// SomeProtocol
protocol SomeProtocol {
    var someValue: String { get }
}

// ClassA that conforms to SomeProtocol and another irrelevant one
class ClassA: SomeProtocol, SomeOtherProtocol {
    var someValue: String { "classA" }
}

// ClassB that conforms to SomeProtocol and another irrelevant one
class ClassB: SomeProtocol, AnotherProtocol {
    var someValue: String { "classB" }
}

// Registration of dependencies via Resolver
register { ClassA() }
    .implements(SomeProtocol.self)
    .implements(SomeOtherProtocol.self)
    .scope(.shared)

register { ClassB() }
    .implements(SomeProtocol.self)
    .implements(AnotherProtocol.self)

// When another instance wants to use SomeProtocol as a dependency, resolver can't solve this ambiguity and provides the last written registration only (which is ClassB) in this case.
class ClassC {
    @Injected var someProtocolInstance: SomeProtocol
}

As seen on the simple example above, different types can conform to the same protocol, yet there is no distinct way to point which of the instances in the implementation. In poor man's dependency injection via constructors, we can always provide the specific concrete instance but the other class doesn't know anything about it since it only cares about the protocol conformance. This way, we can still keep a reference to the same instance and provide the only necessary contract via the protocol.

On the example above, resolver creates a brand new ClassB instance and injects it to ClassC, but in reality what if I want to reuse my shared ClassA instance?

As we know this is a very common way to practice DI while preserving testability and limiting exposure. I am wondering if there is a way to satisfy this via Resolver, if not, how can people use it? Multiple conformances in different types are very common in modern coding.

hmlongco commented 3 years ago

So you start out by discussing interface injection, then show an example of annotation using @Injected.

Regardless, if you want to actually user interface injection...

class ClassC {
    lazy var someProtocol = getSomeProtocol()
    ...
}

extension ClassC: Resolving {
    func getSomeProtocol() -> SomeProtocol { return resolver.resolve(ClassB.self) }
}

Where you explicitly tell Resolver which class you actually want to use for the instance in question.

You can do the same in constructor injection...

register { ClassC(someProtocol: resolve(ClassB.self) }

Bottom line is that Resolver isn't magic. It does type inference, but if you have more than one instance of something you have to differentiate one from the other. Above we do it by explicitly defining the class type.

Another very common solution to the problem is to use namespaces.

The very first example on that page illustrates just the situation you describe, where we want to specify just which provider of the XYZServiceProtocol we want to use at that point in time.

erenkabakci commented 3 years ago

Thank you for the quick response!

So you start out by discussing interface injection, then show an example of annotation using @injected.

Well, "annotations" are indeed only annotations 😅 Does it matter if you annotate a concrete type or an interface? I don't think so.

Anyhow, for the first example you gave, ClassB has to be defined within the same framework/module/namespace in order to work.

Imagine that ClassC implementation and the SomeProtocol declaration is defined in a low level framework e.g. networking. But the actual concrete class injection and the dependency creation happens on the top level. e.g. app target which imports the networking in this case. So in this case classC should define another method in the protocol signature so it can be fulfilled by the top level injector level, as far as I understand. Because ClassC and the framework/target/module has no idea about ClassB and shouldn't 🤷

Now looking at the namespace case you pointed out, that's indeed can be a way to tackle this. I should look into that one. Thanks!

hmlongco commented 3 years ago

I was perhaps being pedantic, but if you follow the first link I gave you to the documentation on Interface Injection you'd see that the term has a specific meaning. An "interface" is injected into an object that allows it to consume an injected service.

The first example I gave was an example of true interface injection.

But I think named interfaces are indeed the solution to the problem at hand.