Building S.O.L.I.D - Dependency Inversion Principle

The Dependency Inversion Principle is a S.O.L.I.D design principle that is best stated by Robert C. Martin in his 2003 book, Agile Software Development, Principles, Patterns, and Practices:

A. High-level modules should not depend on low-level modules. Both should depend on abstractions. B. Abstractions should not depend on details. Details should depend on abstractions.

To fully understand the problem the Dependency Inversion Principle solves, let's look at some example code:

public class ReportRetriever()
{
    /*
        This references an implemented, low-level module.
    */
    ReportRepository reportRepository;

    public ReportRetriever(ReportRepository reportRepo)
    {
        reportRepository = reportRepo;
    }

    public Report GetReport(Guid reportId)
    {
        var report = reportRepository.findById(reportId);
        return report;
    }
}

In the example above, we can observe that the GetReport function in the ReportRetriever class relies heavily on the implementation of the ReportRepository. That is, our higher-level module (ReportRetriever) is dependent on our lower-level module (ReportRepository). This violates the Dependency Inversion Principle. If in future we wanted to test the ReportRetriever, we will face issues in coupling between the references to the low-level module & the high-level class we want to test.

To rectify this, we need to introduce an abstraction. In this particular example, we can extract an interface for the ReportRepository and use that as a contract:

/*
    This defines a abstraction that high-level modules can
    rely on, and low-level modules can implement. 
*/
public interface IReportRepository()
{
    public Report GetReport(Guid reportId);
}

public class ReportRetriever()
{
    /*
        By accepting an interface, the high-level module will 
        rely on an abstraction, not the implementation.
    */
    IReportRepository _reportRepository;

    public ReportRetriever(IReportRepository reportRepository)
    {
        /*
            The implementation can be specified in the constructor, 
            but is more commonly injected with a Dependency 
            Injection framework.  
        */
        _reportRepository = reportRepository;
    }

    public Report GetReport(Guid reportId)
    {
        var report = _reportRepository.findById(reportId);
        return report;
    }
}

Our ReportRetriever now relies on an abstraction of the low-level module, rather than the implementation. If we needed to test our ReportRetriever class, we could swap out the implementation in the ReportRetriever for a mock class, separating the ReportRetriever code from the repository code. Likewise, if we needed to change how the data was accessed, we are now able to supply an alternative to the ReportRepository, as long as it satisfies the same interface.

One common way to facilitate dependency inversion is to utilize a dependency injection framework - however, dependency injection is not equivalent to dependency inversion; it is one way of achieving a level of dependency inversion, and will be covered in a future post.

In review, the Dependency Inversion Principle allows us to write code that is/has:

  • Easier to test (as dependency inversion allows us to swap out implementations, such as in the case of mocking objects in unit testing)
  • Easier to change (as long as our new changes/models follow our contracts/interfaces)
  • Low coupling between high-level (orchestration) modules and low-level modules

This article is my 5th oldest. It is 477 words long, and it’s got 0 comments for now.