Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Dependency Inversion Principle (DIP)

4.55/5 (20 votes)
31 Jan 2021CPOL10 min read 33.4K  
How Dependency Inversion Principle works, Inversion of Control Container
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:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

For example, the code below does not comply to the above principles:

C#
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()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

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.

C#
public interface IOperation
{
  void Initiate();
  void Send();
}

public class LowLevelModule: IOperation
{
  public void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

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.

C#
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.

C#
public interface IOperation
{
  void Send();
}

public class LowLevelModule: IOperation
{
  public LowLevelModule()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

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:

  1. Using an Interface
  2. Using an Abstract Class
  3. 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.

C#
public abstract class OperationBase
{
  public abstract void Send();
}

public class LowLevelModule: OperationBase
{
  protected LowLevelModule()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending
  }
  
  public override void Send()
  {
    //perform sending operation
  }
}

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.

C#
public interface IOperation
{
  void Send();
}

public abstract class OperationBase: IOperation
{
  protected OperationBase()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending, also shared implementation in this example
  }

  public abstract void Send();
}

public class LowLevelModule: OperationBase
{
  public override void Send()
  {
    //perform sending operation
  }
}

public class AnotherLowLevelModule: OperationBase
{
  public override void Send()
  {
    //perform another sending operation
  }
}

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.

C#
public class Caller
{ 
  public void CallerMethod()
  {
    var module = new HighLevelModule(Send);
    ...
  }

  public void Send()
  {
    //this is the method injected into HighLevelModule
  }
}

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.

C#
public delegate void SendOperation();

public class Caller
{ 
  public void CallerMethod()
  {
    var module = new HighLevelModule(Send);
    ...
  }

  public void Send()
  {
    //this is the method injected into HighLevelModule
  }
}

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:

  1. Using Dependency Injection
  2. Using Global States
  3. 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:

C#
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:

C#
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:

C#
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.

C#
public class Caller
{ 
  public void CallerMethod()
  {
    var module = new HighLevelModule();
    module.SendEvent += Send ;

    ...
  }

  public void Send()
  {
    //this is the method injected into HighLevelModule
  }
}

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.

C#
  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:

C#
public interface IOperation
{
  void Send();
}

public class LowLevelModule: IOperation
{
  public LowLevelModule()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

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()
  {
     //registry the LowLevelModule as implementation of IOperation
     var register  = new Registry();
     registry.For<IOperation>.Use<LowLevelModule>();

     //wrap-up registry in a container
     var container = new Container(registry);
      
     //inject the container into HighLevelModule
     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:

C#
public interface IOperation
{
  void Send();
}

public class LowLevelModule: IOperation
{
  public LowLevelModule()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

public interface IModuleFactory
{
   IOperation CreateModule();
}

public class ModuleFactory: IModuleFactory
{
  public IOperation CreateModule()
  {
      //LowLevelModule is the implementation of the IOperation, 
      //and it is hardcoded in the factory. 
      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()
  {
     //create the factory as the implementation of abstract factory
     IModuleFactory moduleFactory = new ModuleFactory();
      
     //inject the factory into HighLevelModule
     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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)