Dependency Inversion Principle

In the previous chapter, we learned about implementing IoC principle using Factory pattern and achieved the first level of loosely coupled design. Here, we will learn and implement Dependency Inversion Principle as the second step to achieve loosely coupled classes.

First of all, let's understand what is Dependency Inversion Principle (DIP)?

DIP is one of the SOLID object oriented principle invented by Robert Martin (a.k.a. Uncle Bob)

DIP Definition

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

To understand DIP, let's take an example from the previous chapter as shown below.

public class CustomerBusinessLogic
{
    public CustomerBusinessLogic()
    {
    }

    public string GetCustomerName(int id)
    {
        DataAccess _dataAccess = DataAccessFactory.GetDataAccessObj();

        return _dataAccess.GetCustomerName(id);
    }
}

public class DataAccessFactory
{
    public static DataAccess GetDataAccessObj() 
    {
        return new DataAccess();
    }
}

public class DataAccess
{
    public DataAccess()
    {
    }

    public string GetCustomerName(int id) {
        return "Dummy Customer Name"; // get it from DB in real app
    }
}

In the above example, we implemented factory pattern to achieve IoC. But, CustomerBusinessLogic class uses concrete DataAccess class. So still it is tightly coupled even though we have inverted the dependent object creation to the factory class.

Let's use DIP on the CustomerBusinessLogic and DataAccess classes and make them more loosely coupled.

As per DIP definition, a high-level module should not depend on low-level modules. Both should depend on abstraction. So, first, decide which is the high-level module (class) and low-level module. High-level module is a module which depends on other modules. In our example, CustomerBusinessLogic depends on DataAccess class, so CustomerBusinessLogic is high-level module and DataAccess is low-level module. So, as per first rule of DIP, CustomerBusinessLogic should not depends on concrete DataAccess class, instead both classes depends on abstraction.

The second rule in DIP is "Abstractions should not depend on details. Details should depend on abstractions".

What is Abstraction?

Abstraction and encapsulation are important principles of object-oriented programming. There are many different definitions from many people but let's understand abstraction using the above example.

In English, abstraction means something which is non-concrete. In programming terms, the above CustomerBusinessLogic and DataAccess are concrete classes, meaning we can create objects of it. So, abstraction in programming is to create an interface or abstract class which is non-concrete. This means we cannot create an object of interface or abstract class. As per DIP, CustomerBusinessLogic (high-level module) should not depend on concrete DataAccess (low-level module) class. Both classes depend on abstractions, meaning both classes should depend on interface or abstract class.

Now, what should be in interface (or in abstract class)? As you can see, CustomerBusinessLogic uses GetCustomerName() method of DataAccess class. (In real life, there will be many customer related methods in DataAccess class). So, let's declare GetCustomerName(int id) method in the interface as shown below.

public interface ICustomerDataAccess
{
    string GetCustomerName(int id);
}

Now, implement ICustomerDataAccess in CustomerDataAccess class as shown below. (So instead of DataAccess class, let's define new CustomerDataAccess class.)

public class CustomerDataAccess: ICustomerDataAccess
{
    public CustomerDataAccess()
    {
    }

    public string GetCustomerName(int id) {
        return "Dummy Customer Name";        
    }
}

Now, we need to change our factory class which returns ICustomerDataAccess instead of concrete DataAccess class as shown below.

public class DataAccessFactory
{
    public static ICustomerDataAccess GetCustomerDataAccessObj() 
    {
        return new CustomerDataAccess();
    }
}

Now, change the CustomerBusinessLogic class which uses ICustomerDataAccess instead of concrete DataAccess class as shown below.

public class CustomerBusinessLogic
{
    ICustomerDataAccess _custDataAccess;

    public CustomerBusinessLogic()
    {
        _custDataAccess = DataAccessFactory.GetCustomerDataAccessObj();
    }

    public string GetCustomerName(int id)
    {
        return _custDataAccess.GetCustomerName(id);
    }
}

Thus, we have implemented DIP in our example where high-level module (CustomerBusinessLogic) and low-level module (CustomerDataAccess) are depends on abstraction (ICustomerDataAccess). Also, abstraction (ICustomerDataAccess) does not depends on details (CustomerDataAccess) but details depend on abstraction.

The following is the complete DIP example discussed so far.

Example: DIP Implementation
public interface ICustomerDataAccess
{
    string GetCustomerName(int id);
}

public class CustomerDataAccess: ICustomerDataAccess
{
    public CustomerDataAccess() {
    }

    public string GetCustomerName(int id) {
        return "Dummy Customer Name";        
    }
}

public class DataAccessFactory
{
    public static ICustomerDataAccess GetCustomerDataAccessObj() 
    {
        return new CustomerDataAccess();
    }
}

public class CustomerBusinessLogic
{
    ICustomerDataAccess _custDataAccess;

    public CustomerBusinessLogic()
    {
        _custDataAccess = DataAccessFactory.GetCustomerDataAccessObj();
    }

    public string GetCustomerName(int id)
    {
        return _custDataAccess.GetCustomerName(id);
    }
}

Advantages of implementing DIP in the above example is that CustomerBusinessLogic and CustomerDataAccess classes are loosely coupled classes because CustomerBusinessLogic does not depend on concrete DataAccess class, instead it includes reference of ICustomerDataAccess interface. So now, we can easily use another class which implements ICustomerDataAccess with different implementation.

Still, we have not achieved fully loosely coupled classes because CustomerBusinessLogic class includes Factory class to get the reference of ICustomerDataAccess. This is where Dependency Injection pattern helps us. In the next chapter, we will learn how to use DI and Strategy pattern using the above example.