In this article, we will discuss about the working principles of Dependency Inversion Principle, and how to apply it to a working example.
Introduction
In this article, we will discuss about one of the pillars in SOLID principles, which is Dependency Inversion Principle. We will discuss the working principles behind it, and how to apply it to a working example.
1. Concepts
What is DIP?
The principle states:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
For example, the code below does not comply to the above principles:
public class HighLevelModule
{
private readonly LowLevelModule _lowLowelModule;
public HighLevelModule()
{
_lowLevelModule = new LowLevelModule();
}
public void Call()
{
_lowLevelModule.Initiate();
_lowLevelModule.Send();
}
}
public class LowLevelModule
{
public void Initiate()
{
}
public void Send()
{
}
}
In the above code, HighLevelModule
depends directly on LowLevelModule
and this does not follow the first point of DIP. Why does this matter? The direct and tightly coupled relationship between the two makes it harder to create unit tests on HighLevelModule
in isolation from LowLevelModule
. You are forced to test HighLevelModule
and LowLevelModule
at the same time because they are tightly coupled.
Note, it is still possible to do unit tests on HighLevelModule
in isolation using testing framework which performs .NET CLR interception, such as TypeMock Isolator. Using this framework, it is possible to alter LowLevelModule
behavior on testing. However, I don't recommended this practice for two reasons. Firstly, using CLR interception in testing defies the reality of the code: the reliance of HighLevelModule
on LowLevelModule
. At worst, the testing can give false positive results. Secondly, that practice may discourage us to learn the skills to write clean and testable code.
How Do We Apply DIP?
The first point of DIP suggest us to apply two things at the same time to the codes:
- Abstraction
- Dependency Inversion or Inversion of Control
Firstly, LowLevelModule
needs to be abstracted and HighLevelModule
will depend on the abstraction instead. Different methods of abstraction will be discussed in the next section. For the example below, I will use an interface
for the abstraction. An IOperation
interface is used to abstract LowLevelModule
.
public interface IOperation
{
void Initiate();
void Send();
}
public class LowLevelModule: IOperation
{
public void Initiate()
{
}
public void Send()
{
}
}
Secondly, because HighLevelModule
will solely depend on IOperation
abstraction, we can't have new LowLevelModule()
inside the HighLevelModule
class anymore. LowLevelModule
needs to be injected into HighLevelModule
class from the caller context. The dependency, LowLevelModule
, needs to be inverted. This is where the terminology 'Dependency Inversion' and 'Inversion of Control' come from.
The implementation of the abstraction, LowLevelModule
, or behavior need to be passed from outside of HighLevelModule
, and the process of moving this from inside to outside of the class is called inversion. I will discuss different methods of dependency inversion in section 3. In the example below, dependency injection via constructor will be used.
public class HighLevelModule
{
private readonly IOperation _operation;
public HighLevelModule(IOperation operation)
{
_operation = operation;
}
public void Call()
{
_operation.Initiate();
_operation.Send();
}
}
We have decoupled the HighLevelModule
and LowLevelModule
from each other, and both now depend on the abstraction IOperation
. The Send
method behavior can be controlled from outside of the class, by passing any implementation choices of IOperation
, e.g., LowLevelModule
However, it is not finished yet. The code still does not comply with the second point of DIP. The abstraction should not depend on the detail or implementation. The Initiate
method in IOperation
is in fact an implementation detail of LowLevelModule
, which is used to prepare the LowLevelModule
before it can perform Send
operation.
What I have to do is remove it from the abstraction, IOperation
, and consider this as part of LowLevelModule
implementation details. I can include the Initiate
operation inside LowLevelModule
constructor. This allows the operation to be a private
method, limiting its access to within the class.
public interface IOperation
{
void Send();
}
public class LowLevelModule: IOperation
{
public LowLevelModule()
{
Initiate();
}
private void Initiate()
{
}
public void Send()
{
}
}
public class HighLevelModule
{
private readonly IOperation _operation;
public HighLevelModule(IOperation operation)
{
_operation = operation;
}
public void Call()
{
_operation.Send();
}
}
2. Abstraction Methods
The first activity in implementing DIP is to apply abstractions to the parts of the codes. In the C# world, there are a few ways to do this:
- Using an Interface
- Using an Abstract Class
- Using a Delegate
First, an interface
is solely used to provide an abstraction, while an abstract class
can also be used to provide some shared implementation details. Lastly, a delegate provides an abstraction for one particular function or method.
As a side note, it is a common practice to mark a method as virtual, so the method can be mocked when writing unit tests for the calling class. However, this is not the same as applying an abstraction. Marking a method as virtual just makes it overridable, so the method can be mocked and this can be useful for testing purposes.
My preference is to use an interface
for an abstraction purposes. I use an abstract
class only if there is a shared implementation detail between two or more classes. Even that, I will make sure that the abstract
class implements an interface
for the actual abstraction. In section 1, I already give examples of applying abstractions using interfaces
. In this section, I will give other examples using an abstract
class and a delegate.
Using an Abstract Class
Using the example in section 1, I just need to change the interface IOperation
to an abstract
class, OperationBase
.
public abstract class OperationBase
{
public abstract void Send();
}
public class LowLevelModule: OperationBase
{
protected LowLevelModule()
{
Initiate();
}
private void Initiate()
{
}
public override void Send()
{
}
}
public class HighLevelModule
{
private readonly OperationBase _operation;
public HighLevelModule(OperationBase operation)
{
_operation = operation;
}
public void Call()
{
_operation.Send();
}
}
The above codes are equivalent to using an interface
. I normally only use abstract
classes if there is a shared implementation detail. For example, if HighLevelModule
can use either LowLevelModule
or AnotherLowLevelModule
, and both classes have a shared implementation detail, then I will use an abstract
class as a base class for both. The base class will implement IOperation
, which is the actual abstraction.
public interface IOperation
{
void Send();
}
public abstract class OperationBase: IOperation
{
protected OperationBase()
{
Initiate();
}
private void Initiate()
{
}
public abstract void Send();
}
public class LowLevelModule: OperationBase
{
public override void Send()
{
}
}
public class AnotherLowLevelModule: OperationBase
{
public override void Send()
{
}
}
public class HighLevelModule
{
private readonly IOperation _operation;
public HighLevelModule(IOperation operation)
{
_operation = operation;
}
public void Call()
{
_operation.Send();
}
}
Using a Delegate
A single method or function can be abstracted using a delegate. Generic delegate Func<T>
or Action
can be used for this purpose.
public class Caller
{
public void CallerMethod()
{
var module = new HighLevelModule(Send);
...
}
public void Send()
{
}
}
public class HighLevelModule
{
private readonly Action _sendOperation;
public HighLevelModule(Action sendOperation)
{
_sendOperation = sendOperation;
}
public void Call()
{
_sendOperation();
}
}
Alternatively, you can create your own delegate and give a meaningful name to it.
public delegate void SendOperation();
public class Caller
{
public void CallerMethod()
{
var module = new HighLevelModule(Send);
...
}
public void Send()
{
}
}
public class HighLevelModule
{
private readonly SendOperation _sendOperation;
public HighLevelModule(SendOperation sendOperation)
{
_sendOperation = sendOperation;
}
public void Call()
{
_sendOperation();
}
}
The benefit of using generic delegates is that we don't need to create or implement a type, e.g., interfaces and classes, for the dependencies. We can just use any methods or functions from the caller context or from anywhere else.
3. Dependency Inversion Methods
In section one, I use constructor dependency injection as the dependency inversion method. In this section, I will discuss various methods of dependency inversion methods.
Here the list of dependency inversion methods:
- Using Dependency Injection
- Using Global States
- Using Indirection
Below, I will explain each of the methods.
1. Using Dependency Injection
Using Dependency Injection(DI), is where the dependency is injected directly to a class via its public members. The dependency can be injected into class's constructor (Contructor Injection), set property (Setter Injection), method (Method Injection), events, index properties, fields and basically any members of the class which are public
. I generally don't recommended to use fields because exposing fields are not considered a good practice in Object-Oriented programming, as you can achieve the same thing using properties. Using index properties for dependency injection is also a rare case, so I will not explain it further.
Constructor Injection
I used mostly Constructor Injection. Using Constructor Injection can also leverage on some features in IoC container, such as auto-wiring or type discovery. I will discuss about IoC container in section 5 later. Below is an example of Construction Injection:
public class HighLevelModule
{
private readonly IOperation _operation;
public HighLevelModule(IOperation operation)
{
_operation = operation;
}
public void Call()
{
_operation.Send();
}
}
Setter Injection
Setter and Method Injection are used to inject dependencies after the construction of the object. This can be seen as disadvantages when using along with IoC container (will be discussed in section 5). However, if you don't use IoC container, they achieve the same thing as the Constructor Injection. The other benefit of Setter or Method Injection is to allow you to alter the dependency on the runtime, and they can be used in complement of Constructor injection. Below is an example of Setter Injection, which allows you to inject one dependency at a time:
public class HighLevelModule
{
public IOperation Operation { get; set; }
public void Call()
{
Operation.Send();
}
}
Method Injection
The Method Injection allows you to set multiple dependencies at the same time. Below is the example of Method Injection:
public class HighLevelModule
{
private readonly IOperation _operationOne;
private readonly IOperation _operationTwo;
public void SetOperations(IOperation operationOne, IOperation operationTwo)
{
_operationOne = operationOne;
_operationTwo = operationTwo;
}
public void Call()
{
_operationOne.Send();
_operationTwo.Send();
}
}
When using Method Injection, the dependencies that are passed as arguments will be persisted in the class, e.g., as fields or properties, for later use. When passing some classes or interfaces in the method and to be used just in the method, this does not count as Method Injection.
Using Events
Using Events is limited only for delegate type injection, and this is only appropriate where subscription and notificatin model is required, and the delegate must not return any value, or just returning void
. The caller will subscribe a delegate to the class that implements the event, and there can be multiple subscribers. The event injection can be performed after the object construction. Injecting events via constructor is uncommon. Below is an example of event injection.
public class Caller
{
public void CallerMethod()
{
var module = new HighLevelModule();
module.SendEvent += Send ;
...
}
public void Send()
{
}
}
public class HighLevelModule
{
public event Action SendEvent = delegate {};
public void Call()
{
SendEvent();
}
}
In general, my mantra is always use Constructor Injection if nothing compelling to use Setter or Method Injection, and this is also to enable us to use IoC container later.
2. Using Global States
Instead of injected directly into the class, the dependency can be retrieved from a global state from inside the class. The dependency can be injected into the global states and later accessed from inside the class.
public class Helper
{
public static IOperation GlobalStateOperation { get; set;}
}
public class HighLevelModule
{
public void Call()
{
Helper.GlobalStateOperation.Send();
}
}
public class Caller
{
public void CallerMethod()
{
Helper.GlobalStateOperation = new LowLevelModule();
var highLevelModule = new HighLevelModule();
highLevelModule.Call();
}
}
}
Global states can be represented as properties, methods or even fields. The important bit is that the underlying value has public setter and getter. The setter and getter can be in the form of methods instead of properties.
If the global state has only getter (e.g., singleton), the dependency is not inverted. Using global states to invert dependencies is not recommended, as it makes dependencies less obvious, and hides them inside the class.
3. Using Indirection
If you are using Indirection, you don't pass dependency directly into the class. Instead, you pass an object that is capable of creating or passing the implementation of the abstraction for you. This also means that you create another dependency for the class. The type of object you pass into the class can be:
- Registry/Container object
- Factory object
You can choose whether to pass the object directly (Dependency Injection) or using Global States.
Registry/Container object
If you use a register, this often called Service Locator Pattern, then you can query the register to return an implementation of an abstraction (e.g. interface). However, you will need to register the implementation first from outside the class. You can also use a container to wrap up the registry like many IoC container frameworks do. A container normally has additional features such type discovery or auto-wiring, so when you register an interface
and its implementation, you don't need to specify the dependencies of the implementation class. When you query the interface, the container will be able to return the implementation class instance by resolving all its dependencies first. Of course, you will need to register all the dependencies first.
In the early days when IoC container frameworks just sprung up, the container was often implemented as a Global state or Singleton instead of explicitly passing it into a class, and this was now considered as anti-pattern. Here is an example of using a container type object:
public interface IOperation
{
void Send();
}
public class LowLevelModule: IOperation
{
public LowLevelModule()
{
Initiate();
}
private void Initiate()
{
}
public void Send()
{
}
}
public class HighLevelModule
{
private readonly Container _container;
public HighLevelModule(Container container)
{
_container = container;
}
public void Call()
{
IOperation operation = _container.Resolvel<IOperation>();
operation.Send();
}
}
public class Caller
{
public void UsingContainerObject()
{
var register = new Registry();
registry.For<IOperation>.Use<LowLevelModule>();
var container = new Container(registry);
var highLevelModule = new HighLevelModule(container);
highLevelModule.Call();
}
}
You can even make HighLevelModule
depends on the abstraction of the container, but this step is not necessary.
Moreover, using a container or registry everywhere in the classes may not be a good idea, as this makes it less obvious what the class's dependencies are.
Factory Object
The difference between using a register/container and a factory object is that when using a register/container, you need to register the implementation class before you can query it, while using a factory you don't need to do that as the instantiation is hardcoded in the factory implementation. Factory object is not neccesary to have 'factory' as parts of its name. It can be just a normal class that returns an abstraction (e.g., interface).
Moreover, because LowLevelModule
instantiation is hardcoded in the factory implementation, having HighLevelModule
depends on the factory will not invert the LowLevelModule
dependency. In order to invert the dependency, HighLevelModule
needs to depend on the factory abstraction instead, and the factory object needs to implement that abstraction. Here is an example of using a factory object:
public interface IOperation
{
void Send();
}
public class LowLevelModule: IOperation
{
public LowLevelModule()
{
Initiate();
}
private void Initiate()
{
}
public void Send()
{
}
}
public interface IModuleFactory
{
IOperation CreateModule();
}
public class ModuleFactory: IModuleFactory
{
public IOperation CreateModule()
{
return new LowLevelModule();
}
}
public class HighLevelModule
{
private readonly IModuleFactory _moduleFactory;
public HighLevelModule(IModuleFactory moduleFactory)
{
_moduleFactory = moduleFactory;
}
public void Call()
{
IOperation operation = _moduleFactory.CreateModule();
operation.Send();
}
}
public class Caller
{
public void CallerMethod()
{
IModuleFactory moduleFactory = new ModuleFactory();
var highLevelModule = new HighLevelModule(moduleFactory);
highLevelModule.Call();
}
}
My recommendation is to use indirection sparingly. Service Locator Pattern, is seen as anti-pattern nowadays. However from time to time, you may need to use a factory object to create the dependencies for you. My Ideal is to stay clear from using Indirection, unless it is proven necessary.
Besides the implementation of abstraction (interface, abstract class or delegate), we normally can also inject dependencies for primitive type such as Boolean, int
, double
, string
or just a class which contain only properties.
Points of Interest
Additionally, I also wrote about how to apply DIP to existing code and the role of IOC container in that process.
History
- 2nd October, 2016: Initial version