12 August 2014
In my last post, I explained some of the differences between Dependency Injection (DI) and the factory pattern. I also gave an example of how constructor injection could be used to leverage the DI framework's ability to manage dependencies of dependencies. While constructor injection is my personal preferred way of doing it, there is an alternative. You could also use setter injection.
Using the above code, the structure of the code is still improved without using the constructor injection pattern. And, with some frameworks (Castle Windsor included), the dependencies are loaded automatically just as in the constructor injection examples in my last post. Additionally, the code is still highly testable.
Setter Injection Advantages
- Much better for testing than "newing up" the dependencies in-line (this is also true for constructor injection)
- Great for metadata driven applications (i.e. applications whose look and/or behavior is driven by data)
Setter Injection Disadvantages
- Allows you to "reconfigure" your object
- This could lead to temporal bugs/confusion
- If objects are configured to use the "singleton" life-cycle, which is the default configuration in many frameworks, there will be cross-threaded issues that can be difficult to diagnose
- Constructor-Injection is less susceptible to this problem
- Null checks are necessary
- Constructor-Injection will throw an exception if it cannot find a dependency. This is a "fail-fast" approach, which gives a clear error message and is usually easy to diagnose.
- Since properties are not required to construct a class, the DI framework will not check to see if all of the dependencies are registered in the container.
- If a dependency property is not set (i.e. is null) then a null reference exception is thrown. And, these types of exceptions are notoriously difficult to diagnose because they do not give adequate information.
- Null checks clutter the code with if statements and increases cyclomatic complexity.
- Null checks also need to be tested for in your collection of unit tests. These sorts of tests should be avoided because they offer little or no additional value to the developers or the business and they clutter up your testing suite.
- It makes it more difficult to determine the cohesiveness of a class because it obscures just how many dependencies are actually used by the class
Models verses Functional Classes
In my experience, when compared to constructor-injection, the disadvantages of setter-injection out weigh the benefits. Therefore, most of the time, my applications will use constructor-injection instead. However, there are times when I definitely use setter-injection. Out of all of these advantages, the only one that has been useful to me is configuring metadata driven applications. By this, I am referring to an application whose look and/or behavior is driven by data and not entirely at design time. For example, configuring templates to be rendered on the user interface or configuring rules in a rules framework.
The difference when configuring DI for a metadata driven application is that the application is generally configuring data models and not classes that contain logic (e.g. controllers and commands). Data models are about simple data and types, and these simple types have a way of proliferating over time. A data model class can have many class-level variables and/or properties and still be a cohesive unit. Functional classes on the other hand are about functionality, and the DI framework is useful for managing pieces of logic that the functional class depends on. For functional classes, more than a couple of class-level variables is usually an indication of bad cohesiveness.
There are other differences in the way that models should be handled in DI verses a functional class.
- The life-cycle of models should always be per-call. This is because data that is changed on one thread can have an unexpected consequence on another thread. With functional classes and constructor-injection, this is not usually an issue.
- Models should never be wrapped in an Aspect Oriented Programming (AOP) proxy. This is because doing so would make most serializers throw an exception when the model is serialized. Worse than getting a serialization exception is that when a model is deserialized, it will not be wrapped in an AOP proxy. Therefore, you may not be getting the behavior you are expecting. Again, this is typically not a concern with functional classes.
Because of these differences, it makes more sense to use the appropriate pattern for the type of class than it does to make a blanket statement or set of governances that subscribe one pattern over another. Therefore, it is perfectly acceptable to use both in the same application.
While setter-injection can be an alternative to constructor-injection, it is not a direct replacement and is not as well suited for managing functional classes. However, it is more suited for configuring data models. Therefore, setter-injection should be thought of as another tool in your tool box that has a different purpose.