For reference, in case Wikipedia gets edited, the example given is this:
public interface ServiceSetter {
public void setService(Service service);
}
public class Client implements ServiceSetter {
private Service service;
@Override
public void setService(Service service) {
if (service == null) {
throw new InvalidParameterException("service must not be null");
}
this.service = service;
}
}
public class ServiceInjector {
private Set<ServiceSetter> clients;
public void inject(ServiceSetter client) {
this.clients.add(client);
client.setService(new ExampleService());
}
public void switch() {
for (Client client : this.clients) {
client.setService(new AnotherExampleService());
}
}
}
public class ExampleService implements Service {}
public class AnotherExampleService implements Service {}
First of all, I don't like this example. It has many flaws, too numerous to list here. It feels like some abstract theoretical example drafted by some academic that does not realistically reflect real code. However, once I start picking through the bits of code, it does eventually highlight the key components of dependency injection, it just does it in a very confusing way.
ServiceSetter
isn't being used here in the abstract. Its name is actually very concretely referring to the Service
base type implemented by ExampleService
and AnotherExampleService
. If I had a DatabaseContext
class that was used as a dependency, I would analogously create a DatabaseContextSetter
interface to provide the setter method for the DatabaseContext
dependency.
In other words, implementations of the ServiceSetter
interface are publicly stating "I have a dependency on Service
", and it is necessary for them to make that publicly known (I'll explain why in a second)
Having a dependency inherently means that you need to be able to receive that dependency, hence why the interface inherently requires the implementor to implement a setter method.
This code feels like it predates DI containers, and seems to be built in the expectation that you need to roll your own DI service provider. In that perspective, it makes sense why you'd want a marker interface here to figure out a certain consumer's dependencies. This would allow a homebrew DI service provider to do something along the lines of:
Note: I'm using C# because I'm more familiar with it and I'm better able to keep the code both readable and syntactically correct.
public class ServiceProvider
{
public T Create<T>()
{
var t = new T();
if(t is ServiceSetter ss)
ss.setService(this.Create<Service>());
return t;
}
}
I've hardcoded the ServiceSetter
and Service
types in this example, but you could more realistically refactor this to use a collection of registered types (which is how most DI containers work under the hood), e.g. foreach(var registeredType in this.registeredTypes) { ... }
In verbal terms, this logic uses the interface (ServiceSetter
) to:
- Confirm that the current object we're instantiating has a dependency on
Service
(because it implements ServiceSetter
).
- Create a
Service
instance
- Inject the created
Service
instance into the current object we're instantiating, using the setService
methods that we know it exposes (since it implements the ServiceSetter
interface, which mandates this setter method to exist).
As you can see, the existence of the ServiceSetter
interface is useful in both steps 1 (to identify a consumer) and 3 (ensuring that we have a setter method to inject the dependency into the consumer).
Nowadays, we don't really do dependency injection via method anymore (as far as I've seen), we define our dependencies using the constructor. Constructors can more easily be found (using reflection), and therefore we are already able to answer the questions:
- (Step 1) It is a consumer of
Service
when it has a Service
constructor parameter.
- (Step 3) The constructor inherently operates as the dependency setter method.
Round this all off, I want to circle back to your core question:
Why do we need separate interface for the setter method instead of just having it just in the client class?
You are correct that dependencies are generally declared by a concrete instance, not an abstract interface. However, as explained above, the interface is mostly likely used here in relation to how a DI service provider would supply those dependencies.
Since we've mostly started using constructor-based DI now, we are in fact registering dependencies in the concrete class (since a constructor is about as personal as it gets to a class and not some abstraction layer on top of it).