Reducing Code Coupling – Dependency Injection
Introduction
Previously, I talked a bit about the nature of coupling in code, and the general preference to keep coupling loose where possible. I then showed one way to reduce coupling by using a service locator.
There are multiple techniques to reduce coupling – the service locator is one. In this article, I’ll be illustrating the concept of Dependency Injection, showing how it is done, and comparing it to the service locator technique.
About Dependency Injection
When discussing the service locator, I showed how code might normally create required dependencies on its own. Using the service locator, one can retrieve dependencies that implement given interfaces, which are registered with the service locator. DI works differently in that code using the technique neither creates dependencies nor retrieves them from elsewhere. Instead, such dependencies are provided, or injected, into a class that needs those dependencies. Hence, Dependency Injection.
As has been set down earlier in the series, a "best practice" of programming is to keep coupling loose when possible. The primary benefit to the practice is code that is easier to change. It becomes easier to substitute one implementation of some component or service for another. A secondary benefit is code that is easier to test. This effect shall be demonstrated shortly.
Dependency injection allows you to decouple high-level code from low-level code such as services. In doing so, the code is following the Dependency Inversion Principle, or DIP. DIP, essentially, indicates that high level code should not – directly – depend on lower level code, but rather on abstractions. Also, abstractions should not depend on details, but only on other abstractions.
A common phrase used when explaining DI is "tell, don’t ask". The meaning is that a class with dependencies should not ask for (create or retrieve) them. Rather, it should tell consuming code which dependencies should be injected. How that works is explained below.
There are 3 main types of dependency injection, which vary in how the injection is done. The end result is the same: dependencies are provided to a class from outside the same class.
- Constructor injection
- The most common.
- Injection is done when instantiating a class.
- Injected component can be used anywhere within the class.
- Should be used when the injected dependency is required for the class to function.
- Most obvious way of telling consuming code that the class has dependencies.
- Property injection
- Used when a class has optional dependencies, or where the implementations may need to be swapped. Different logger implementations could be used this way.
- May require checking for a provided implementation throughout the class – need to check for null before using it.
- Could be useful for transitioning legacy code towards DI.
- Does not require adding or modifying constructors.
- Is not an obvious up-front indicator of class dependencies.
- Method injection
- Inject the dependency into a single method, for use by that method. Could be useful where the whole class does not need the dependency, just the one method.
- Generally uncommon, usually used for edge cases.
Benefits of Dependency Injection
Coupling
The main benefits of using DI are essentially the same as those for using the service locator: reduced coupling and, as a result, better changeability.
Same as with the service locator, DI helps to reduce coupling by encouraging programming to interfaces. There is an additional bonus to DI: there is no need to use the service locator. I will explain. When you use the service locator to reduce coupling, by programming to interfaces, you are still introducing some coupling by writing code to use the service locator itself. This is a fair trade-off, and your code can come out ahead as a result, but that small amount of coupling will still be there. If carefully managed, the impact can be minimized.
Testability
Testability is somewhat improved when using the service locator, but it requires noticeable extra work to set up a class for testing. You not only have to create the appropriate fake dependencies, you also have to configure the service locator to provide those fakes, thereby complicating your tests. Also, you have to know in advance whether a class internally retrieves a dependency from the service locator.
When using DI, unit testing is made simpler, as there is no need to configure the service locator for test mode. You just create your fake dependencies and inject them into the class under test. This is a big advantage DI has over the service locator. Unit tests should be simple, but they are not simple when the service locator is involved in the test.
Code may involve interaction with a database, a web service, or the file system. As stated before, unit tests should be simple, and they will not be if code under test uses such resources. Just as with service location, DI can be used to inject fake resources to keep tests small, simple, and fast. This discussion of stubs/fakes/mocks is not really the focus of this series, but may receive some coverage at a later time. “Fake” is the term that will be used here.
Initial Example
Following is a snip of code from my ongoing project. Do be aware that I have stripped out irrelevant details, leaving just the code that is the focus of discussion.
Public Class DatasetGroup
...
Public Function CreateChildGroup(ByVal path As String) As DatasetGroup
System.IO.Directory.CreateDirectory(path)
End Function
...
End Class
The class makes direct use of filesystem IO functionality. This is not ideal, as the class is not unit testable (recall that one of the aspects of a unit test is that the class under test should not touch the filesystem). The solution is to wrap the IO functionality in an external class, and inject that class into this one to do the IO work.
Consider the following two code samples:
Public Interface IFileSystem
...
Sub CreateDirectory(ByVal path As String)
...
End Interface
Public Class FileSystem
Implements IFileSystem
...
Public Sub CreateDirectory(ByVal path As String) Implements IFileSystem.CreateDirectory
System.IO.Directory.CreateDirectory(path)
End Sub
...
End Class
The interface defines the required IO functions, and the implementation simply wraps .NET’s IO functionality. To get this implementation into the consuming class, dependency injection is used. The code needs to be amended so that it does not directly interact with the file system; rather, it should receive an IFileSystem implementation and delegate the IO work. In this example, dependency injection is done via the constructor.
Public Class DatasetGroup
Private _fileSystem as IFileSystem
Public Sub New(ByVal fileSystem As IFileSystem)
_fileSystem = fileSystem
End Sub
...
Public Function CreateChildGroup(ByVal path As String) As DatasetGroup
_fileSystem.CreateDirectory(path)
End Function
...
End Class
With this change made, this method can now be unit tested. This is done in a test by creating a fake implementation of the IFileSystem interface and providing that to the class, as shown:
<testFixture()> _
Public Class DatasetGroup_Test
Private _fixture as DatasetGroup
<fixtureSetUp()> _
Public Sub FixtureSetUp()
'Class under test is using faked dependency
_fixture = New DatasetGroup(New FileSystemFake())
End Sub
...
<test()> _
Public Sub CreateChildGroupTest()
'Determine appropriate path parameter to use
_fixture.CreateChildGroup(path)
...
'Test that child group is created
End Sub
...
End Class
All there is to the fake implementation is that it has the methods defined by the interface, but the method bodies are empty. The concept of an IFileSystem does not include a “state”, just functionality. So there is no need for the faked methods to actually do anything. And that is enough to satisfy the code being unit tested in this case.
Again, this is not a very complex example. It was boiled down focus on the dependency injection technique. Any other details would just be noise in this discussion.
Production (non-test) code will be rather similar: create the appropriate implementation – the real one – and pass it in to the class when needed. As you might guess, the creation and passing of the dependency whenever you use the dependent class will become repetitive rather quickly. Read on for a solution.
Poor Man’s Dependency Injection
When doing injection, having to create the dependencies to inject, before constructing an object, can be slightly tedious. However, there is a way to simplify constructor injection. Instead of creating the appropriate dependencies outside of the class and passing them in, the construction and passing can be internalized. This is shown below:
Public Class DatasetGroup
Private _fileSystem as IFileSystem
Public Sub New()
Me.New(New FileSystem())
End Sub
Public Sub New(ByVal fileSystem As IFileSystem)
_fileSystem = fileSystem
End Sub
...
End Class
The code is defining a default constructor (with no parameters), and an overload version that has the necessary dependency as a parameter. The default constructor creates the dependency and passes it to the overload version. This forwarding of one constructor to another is known as constructor chaining.
When doing testing, you can create the appropriate fake dependencies and provide them via the overloaded constructor, as shown earlier. No need to worry about the default constructor in this setting.
Obviously, there is a slight redundancy in having multiple constructors for one class, but they are useful in this kind of situation, where you want to override the behaviour or dependencies. Also, by having the default constructor, you relieve the consumers of this class of having to create and provide the dependencies.
There are some downsides to this, however. First, this technique works best if you know ahead of time which dependency implementation should be created in the default constructor (assuming there are multiple implementations, which may or may not be the case). Also, the class has to have knowledge of the concrete dependency, which somewhat defeats the purpose of creating the abstraction.
The benefits of poor man’s are nice, but are probably outweighed by the downsides. That said, this technique is helpful when transitioning legacy code towards DI.
The next section shows a way to remove the coupling disadvantage.
A Hybrid Approach
To further extend the Poor Man’s approach, there is a way to combine the benefits of both service location and dependency injection. The default constructor can be modified to retrieve the necessary dependency from a service locator (as described in my previous post), and pass that into the overloaded constructor.
Public Class DatasetGroup
Private _fileSystem as IFileSystem
Public Sub New()
Me.New(ServiceLocator.Resolve(Of IFileSystem)())
End Sub
Public Sub New(ByVal fileSystem As IFileSystem)
_fileSystem = fileSystem
End Sub
...
End Class
Having done this, the code enjoys the benefits of both techniques: dependencies can be injected into the class, after having been retrieved from a service locator. The class now needs no knowledge of the service implementations at all – this is the main improvement over standard Poor Man’s. And the ease of testing has been retained, thanks to the overloaded constructor. The testing scenario is the same as for Poor Man’s: construct the fake service implementations and provide them to the overloaded constructor. No need to involve the service locator at all.
This hybrid approach shows that you don’t have to go completely to one technique or the other. Indeed, the two can coexist happily within the same code base, each being used for appropriate situations. This requires some thought and planning, but it can be done. Ideally, the service locator would be referenced at most once per class: within a constructor, as shown.
A Nagging Issue
I didn’t show this specifically, but imagine that a class has more then one dependency. Picture in your mind the overloaded constructor for DatasetGroup, and imagine that the parameter list is expanded to two dependencies, or three. Managing those dependencies will start getting unwieldy, rather quickly. You not only have to update the default constructor, you also have to ensure the appropriate dependencies are registered in the service locator, or are otherwise provided.
Also consider a more complex case, that where a dependency implementation may have dependencies of its own. Imagine the nested dependency tree that might occur. Catering to that situation would become very complex. It can be mitigated by coding the sevice locator to automatically provide dependencies (if possible) for resolved interface implementations, but that is another issue I will not get into right now.
Observations
Same as in the previous article, here are some point-form observations generated in the process of preparing this article.
- There are multiple types of DI, each appropriate for different situations.
- Easiest to add when writing new code; adding to existing code may require refactoring.
- Constructor injection is most common, and probably best for application architecture.
- Injecting dependencies into a class is better than having to retrieve or create them within the class itself.
- Poor Man’s DI can save some work for consuming code, but still retains coupling within the default constructor.
- Hybrid approach has best of both worlds for benefits, and avoids coupling disadvantage of Poor Man’s DI.
- Service location and dependency injection can live and work together in the same code base.
- Doing injection makes testing easier than when relying only on service locator, as DI does not require adding fake dependencies to SL.
Conclusion
I’ve briefly, and hopefully succinctly, explained the benefit of dependency injection, and shown how it is done. I then demonstrated the use of such functionality in writing unit tests for production code, a process which can be simplified thanks to using fake dependencies for the code under test.
The Dependency Injection technique was compared and contrasted with that of the Service Locator. In short, the use of injection is a reasonable indicator of a better-organized code base, as opposed to using service location. However, there is no definite rule as to whether injection always should always be used of service location. It is a subjective matter, and, indeed, the two techniques can be used side-by-side in the same code base to good effect.
Next up is a discussion of inversion of control. I’ll save the details for that article, but here’s a teaser: IoC is a further extension of the combination of Service Location and Dependency Injection, and represents a well-decoupled code base.
Extra Reading
I’ve listed below some links to additional resources on the subject of dependency injection. Between them, they will provide a much deeper insight into DI than the intro I’ve provided.
- Dependency Injection – a book from Manning all about DI, in Java.
- Dependency Injection in .NET – Another book from Manning, focused on .NET. Currently available in early access.
- Inversion of Control Containers and the Dependency Injection pattern – a classic article from Martin Fowler on the subject of DI and IoC
Category:
Programming
0 Comments
Comments closed