SOLID Principles - Dependency Inversion Principle

The last of the SOLID Principles to cover is the letter D which is the Dependency Inversion Principle (DIP).

SOLID

What is the Dependency Inversion Principle (DIP)?

The Dependency Inversion Principle (DIP) states that “Depend on abstraction, not on concretions".

The principle is actually made up of two parts:

  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.

That sounds a bit academic, so let’s break it down with an example.

An Example

Let’s go back to our Order class from the earlier principles.

public class Order
{
    public ICollection<Item> Items { get; set; }
    public Customer Customer { get; set; }

    public OrderRepository orderRepository = new OrderRepository();
    public Logger Logger = new Logger();

    public void PlaceOrder()
    {
        try
        {
            orderRepository.AddOrder(this);
        }
        catch (Exception ex)
        {
            Logger.Log(ex);
        }
    }
}

At first glance this looks fine. We even followed the Single Responsibility Principle (SRP) by moving the database and logging code to their own classes.

But there’s a problem. The Order class is directly creating instances of OrderRepository and Logger. It’s tightly coupled to those specific implementations. If we wanted to change how we store orders or how we log errors, we would have to modify the Order class.

The Order class (high-level module) depends directly on OrderRepository and Logger (low-level modules). This violates the Dependency Inversion Principle (DIP).

Let’s fix this by introducing abstractions.

public interface IOrderRepository
{
    void AddOrder(Order order);
}

public interface ILogger
{
    void Log(Exception ex);
}

public class OrderRepository : IOrderRepository
{
    public void AddOrder(Order order)
    {
        using (var dbContext = new DbContext())
        {
            dbContext.Order.Add(order);
        }
    }
}

public class Logger : ILogger
{
    public void Log(Exception ex)
    {
        System.IO.File.WriteAllText(@"c:\log.txt", ex.ToString());
    }
}

Now we update the Order class to depend on the abstractions instead of the concrete implementations.

public class Order
{
    public ICollection<Item> Items { get; set; }
    public Customer Customer { get; set; }

    private readonly IOrderRepository _orderRepository;
    private readonly ILogger _logger;

    public Order(IOrderRepository orderRepository, ILogger logger)
    {
        _orderRepository = orderRepository;
        _logger = logger;
    }

    public void PlaceOrder()
    {
        try
        {
            _orderRepository.AddOrder(this);
        }
        catch (Exception ex)
        {
            _logger.Log(ex);
        }
    }
}

Now the Order class no longer knows or cares about how orders are stored or how errors are logged. It only knows about the interfaces. The actual implementations are passed in through the constructor.

This is the heart of the Dependency Inversion Principle (DIP). The high-level Order class now depends on abstractions (IOrderRepository and ILogger), and the low-level classes (OrderRepository and Logger) also depend on those same abstractions by implementing them.

Why This Matters

By depending on abstractions instead of concrete implementations, we gain several benefits:

  • Flexibility: We can swap out implementations without changing the Order class. Want to log to a database instead of a file? Just create a new class that implements ILogger.
  • Testability: We can easily create mock or fake implementations for unit testing.
  • Decoupling: Changes to low-level modules don’t ripple up to high-level modules.

You may notice this principle works hand in hand with Dependency Injection. DIP tells us to depend on abstractions, and Dependency Injection is the technique we use to provide those abstractions to our classes.

Wrapping Up

The Dependency Inversion Principle (DIP) completes our journey through the SOLID principles. It encourages us to structure our code so that both high-level and low-level modules depend on abstractions rather than concrete implementations.

When you find yourself using new to create dependencies inside a class, that’s often a sign you might be violating DIP. Consider whether an abstraction and constructor injection would serve you better.

Avatar
Alan P. Barber
Software Developer, Computer Scientist, Scrum Master, & Crohn’s Disease Fighter

I specialize in Software Development with a focus on Architecture and Design.

comments powered by Disqus
Next
Previous

Related