Saturday 7 May 2016

Dependency Inversion Principle: Part 1 - Concepts

In this article I will discuss about one of SOLID principles in the Object Oriented Programming paradigm, which is ‘D” or Dependency Inversion Principle (DIP) or also known as Inversion of Control (IoC).

The topic is divided into several blogs entries as outline above because of its length. It is recommended to follow the exact order of the outline. The discussion is presented with many examples in C# codes to help readers to grasp the underlying 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 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()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

In the above codes, HighLevelModule depends directly on LowLevelModule and this does not follow the first point of DIP. Why does this matters? 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 need 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()
  {
    //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 need to be injected into HighLevelModule class from the caller context. The dependency, LowLevelModule , need 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 the 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 allow 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()
  {
    //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();
  }
}

In the next section I will discuss different methods of abstraction available to us in C# world to perform Dependency Inversion.

No comments:

Post a Comment