Avoiding Setter Injection
A while ago I had a twitter discussion with Igor Wiedler and Matthias Noback about setter injected dependencies being mutable.
When using objects as services in an application then we do not want them to have state. This is because we can them call them many times without the outcome changing. For example, if we have a mailer service that sends emails for us we would expect it to behave the same way whenever we call it in an application. We would not want the service's behaviour to be different because we had already called it somewhere else in the request. We can make sure that we write the internals of services to not change their state in this way. Yet when we use setter injection we take control over this from the service and leave it in the hands of the code consuming the service.
The two main ways of injecting dependencies are constructor and setter injection. A main advantage of constructor injection is that ensures that we inject the dependency. This means that there are no surprises later if it has not been. Another advantage is that the choice of dependency is immutable. Constructing the service fixes it to that dependency (assuming we provide no other means of changing it).
With setter injection, we can call the setter method many times. So the choice of dependency is not immutable with setter injection. We cannot use the service knowing we will get the same results with the same arguments. Other code using the service could have changed the dependency between calls. Anywhere that uses the service can now set a different dependency which will change its behaviour.
For example, let's say we can set a filter on our mailer service. Since this is optional we choose to inject it with a setter method. If we do not want a filter then we do not call the method. We could not expect consistent results if other code as other could change the filter dependency.
We could change the setter to only allow the dependency to be set once. We will need to write extra code needed for this. The main issue it will fall down because the of the optional nature of dependencies provided by setter injection. The behaviour remaining the same throughout the service's lifetime is still not guaranteed. We could use the service before the dependency has been set and again after it has been set with different results.
We can avoid this by injecting the dependency through the constructor and making it an optional argument. Now the dependency is still optional but we are fixing whether it is set at the time of construction.
As an aside, it would be better to refactor away from having optional dependencies. Instead we could move to different classes implementing the service's interface. One without the dependency and one where it is mandatory.
So is there a case for using setter injection at all? One other use is as an "adder" method which adds dependencies to a collection. Here calling it again does not replace the dependency but adds it to the collection. We still have the potential for changing behaviour here by adding additionally dependencies later.
If we want to fix the collection of dependencies what can we do? Well we can fix them at construction time by passing the full collection in as a constructor argument. So, is this always possible? Symfony2 uses adder methods to add dependencies outside of the original service definition. These dependencies are usually added in a compiler pass. The compiler pass finds tagged services and adds them as dependencies to another service. It does this by adding a call to the service's adder method for each dependency in the service's definition. Here is an example from the Symfony documentation:
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class TransportCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('acme_mailer.transport_chain')) {
return;
}
$definition = $container->getDefinition(
'acme_mailer.transport_chain'
);
$taggedServices = $container->findTaggedServiceIds(
'acme_mailer.transport'
);
foreach ($taggedServices as $id => $attributes) {
$definition->addMethodCall(
'addTransport',
array(new Reference($id))
);
}
}
}
The problem here is that it is still possible for code using the service to add more dependencies later. So can we prevent this? In the twitter conversation We discussed several options. One was to have a way of flagging that we have finished adding dependencies. The method could then throw an exception if called after the flag is set. We could do this by adding a method that sets the flag, calling it last in the service definition. Unfortunately, this feels like quite an awkward solution and relies on making sure that you add the call to the locking method.
Another would be to have a builder object that collects the dependencies and then set the service up with them. This requires adding an extra layer of complexity, which is not appealing. In fact, we realised that the service container already is doing this job. Just changing the way the compiler pass works would be enough. The TransportChain class's first constructor argument is now a collection of transports. The compiler pass could now be as follows:
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class TransportCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('acme_mailer.transport_chain')) {
return;
}
$definition = $container->getDefinition(
'acme_mailer.transport_chain'
);
$transports = $definition->getArgument(0);
$taggedServices = $container->findTaggedServiceIds(
'acme_mailer.transport'
);
foreach ($taggedServices as $id => $attributes) {
$transports[] = new Reference($id);
}
$definition->replaceArgument(0, $transports);
}
}
Here we are getting the first argument in the service definition for the transport chain. In this case we are assuming that it is already defined as a collection. We then append the references to the tagged services to the collection keeping any existing services. The argument in the definition is then replaced with the populated collection. We can now remove the addTransport method from the class. This prevents code using the transport chain from making further changes.
So it looks as though we can avoid most instances of setter injection with little difficulty. We then give ourselves the greater safety of constructor injection.