The Dependency Inversion Principle is comprised of two rules:

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

This principle is primarily concerned with reducing dependencies among the code modules.  We can think of it as needing the low-level objects to define contracts that the high-level objects can use, without the high-level objects needing to care about the specific implementation the low-level objects provide.

Please note that the DIP is not quite the same thing as Dependency Injection.

Dependency Inversion Principle - Would you solder a lamp directly to the electrical wiring in a wall?

A Simple Example

Let's imagine that we are building an notifications client (a trite example, I know, but bear with me).  We want to be able send both email and SMS text notifications. Here are some sample classes:

public class Email
{
    public string ToAddress { get; set; }
    public string Subject { get; set; }
    public string Content { get; set; }
    public void SendEmail()
    {
        //Send email
    }
}

public class SMS
{
    public string PhoneNumber { get; set; }
    public string Message { get; set; }
    public void SendSMS()
    {
        //Send sms
    }
}

public class Notification
{
    private Email _email;
    private SMS _sms;
    public Notification()
    {
        _email = new Email();
        _sms = new SMS();
    }

    public void Send()
    {
        _email.SendEmail();
        _sms.SendSMS();
    }
}

Notice that the Notification class, a higher-level class, has a dependency on both the Email class and the SMS class, which are lower-level classes.  In other words, Notification is depending on the concrete implementation of both Email and SMS, not an abstraction of said implementation.  Since DIP wants us to have both high and low-level classes depend on abstractions, we are currently violating the Dependency Inversion Principle.

The two classes are said to have high-coupling.  Remember from the post on the Single Responsibility Principle that we don't want such coupling in our code if it can be avoided, since that increases the risk that both of these classes will need to change if one of them changes implementation.

(One trick you can use to determine how tightly coupled your code is is to look for the new keyword. Generally speaking, the more instances of new keyword you have, the more tightly coupled your code is.)

So, since the SOLID principles are all about reducing dependencies, how can we refactor this code to remove the dependency between Notification and Email/SMS? We need to introduce an abstraction, one that Notification can rely on and that Email and SMS can implement.  Let's call that IMessage.

public interface IMessage
{
    void SendMessage();
}

Next, Email and SMS can implement the IMessage interface:

public class Email : IMessage
{
    public string ToAddress { get; set; }
    public string Subject { get; set; }
    public string Content { get; set; }
    public void SendMessage()
    {
        //Send email
    }
}

public class SMS : IMessage
{
    public string PhoneNumber { get; set; }
    public string Message { get; set; }
    public void SendMessage()
    {
        //Send sms
    }
}

And, finally, we can make Notification depend on the abstraction IMessage rather than its concrete implementations:

public class Notification
{
    private ICollection<IMessage> _messages;

    public Notification(ICollection<IMessage> messages)
    {
        this._messages = messages;
    }
    public void Send()
    {
        foreach(var message in _messages)
        {
            message.SendMessage();
        }
    }
}

With this refactoring, all Notification cares about is that there's an abstraction (the interface IMessage) that can actually send the notification, so it just calls that and calls it a day.

In short, we have allowed both high-level and low-level classes to rely on abstractions, thereby upholding the Dependency Inversion Principle.

Potential Hazards

We cannot just implement a bunch of interfaces and call that DIP.  Creating code just for the sake of having it leads to unnecessary complexity, the mortal enemy of maintainability.  But we can use those interfaces to implement the necessary contracts the high-level objects need to call.

The key word there is necessary.  As with all other coding, we should only implement code that is necessary and provides a benefit to the application.

Is It Worth It?

YES.  As with the other SOLID principles, the primary purpose of Dependency Inversion is to remove dependencies from our code, which is a noble goal.  Dependency Inversion enables us to bake in some change tolerance to our code, to confront change and roll with it, allowing our system to adapt to large requirements and changing rules with as much grace as possible.