How can DI help?
Every software application is inevitable of change. As your code grows and new requirements arrive, the importance of maintaining your codes becomes more tangible, and it is not possible for a software application to go on if it is not maintainable. One of the design principles that lead to producing a maintainable code is known as Separation of Concerns (SoC). The SoC is a broad concept and is not limited to software design; but in the case of composing software components, we can think of SoC as implementing distinct classes, each of which deals with a single responsibility. In the first example, finding a tool is a different concern from doing the operation itself and separating these two concerns is one of the prerequisites for creating a maintainable code.
Separation of concerns, however, doesn't lead to a maintainable code if the sections that deal with concerns are tightly coupled to each other.
Although there are different types of forceps that Sarah may need during the operation, she doesn't need to mention the exact type of forceps which she requires. She just states that she needs forceps, and it is on her assistant to determine which forceps satisfies her need the best. If the exact type that Sarah needs is temporarily not available, the assistant has the freedom to provide her with another suitable type. If the hospital has bought a new type of forceps that the assistant thinks is more suitable, he or she can easily switch to the new one because he or she knows that Sarah doesn't care about the type of forceps as long as it is suitable. In other words, Sarah is not tightly coupled to a specific type of forceps.
The key principle leading to loose coupling is the following, from the Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software):
"Program to an "interface", not an "implementation"."
When we address our dependencies as abstract elements (an interface or abstract class), rather than concrete classes, we will be able to easily replace the concrete classes without affecting the consumer component:
class Surgeon { private IForceps forceps; public Surgeon(IForceps forceps) { this.forceps = forceps; } public void Operate() { forceps.Grab(); //... } }
The Surgeon
class is addressing the interface IForceps
and does not care about the exact type of the object injected into its constructer. The C# compiler ensures that the argument passed to the forceps parameter always implements the IForceps
interface and therefore, existence of the Grab()
method is guaranteed. The following code shows how an instance of Surgeon
can be created providing with a suitable forceps:
var forceps = assistant.Get<IForceps>(); var surgeon = new Surgeon (forceps);
Because the Surgeon
class is programmed to the IForceps
interface rather than a certain type of forceps implementation, we can freely instantiate it with any type of forceps that the assistant object decides to provide.
As the previous example shows, loose coupling (surgeon is not dependent on a certain type of forceps) is a result of programming to interface (surgeon depends on IForceps
) and separation of concerns, (choosing forceps is the assistant's concern, while the surgeon has other concerns) which increases the code maintainability.
Now that we know loose coupling increases the flexibility and gives freedom of replacing the dependencies easily; let's see what else we get out of this freedom other than maintainability. One of the advantages of being able to replace the concrete classes is testability. As long as the components are loosely coupled to their dependencies, we can replace the actual dependencies with Test Doubles such as mock objects. Test Doubles are simplified version of the real objects that look and behave like them and facilitate testing. The following example shows how to unit test the Surgeon
class using a mock forceps as a Test Double:
[Test] public void CallingOperateCallsGrabOnForceps() { var forcepsMock = new Mock<IForceps>(); var surgeon = new Surgeon(forcepsMock.Object); surgeon.Operate(); forcepsMock.Verify(f => f.Grab()); }
In this unit test, an instance of the Surgeon
class is being created as a System Under Test (SUT), and the mock object is injected into its constructor. After calling the Operate
method on the surgeon
object, we ask our mock framework to verify whether the Grab
operation is called on the mock forceps object as expected.
Maintainability and testability are two advantages of loose coupling, which is in turn a product of Dependency Injection. On the other hand, the way an Injector creates the instances of concrete types, can introduce the third benefit of DI, which is the late binding. An Injector is given a type and is expected to return an object instance of that type. It often uses reflection in order to activate objects. So, the decision of which type to activate can be delayed to the runtime. Late binding gives us the flexibility of replacing the dependencies without recompiling the application. Another benefit of DI is extensibility. Because classes depend on abstractions, we can easily extend their functionality by substituting the concrete dependencies.