NDepend Blog

Improve your .NET code quality with NDepend

SOLID Design in C#: The Dependency Inversion Principle (DIP) with Examples

November 8, 2023 8 minutes read

SOLID Principle - Dependency Inversion Principle

The Dependency Inversion Principle (DIP) is one of the five essential SOLID design principles. These principles are guidelines for the proper usage of object-oriented features. The DIP definition is:

a. High-level modules should not depend on low-level modules. Both should depend on abstractions.
b. Abstractions should not depend on details (concrete implementation). Details should depend on abstractions.

The DIP was introduced in the 90s by Robert C Martin. Here is the original article.

SOLID Principles Summary

SOLID Principles - Dependency Inversion Principle

Before delving into the Dependency Inversion Principle, let’s take a quick look at how it stands in the SOLID design principles:

  • The Single Responsibility Principle (SRP): A class should have one reason to change. This principle is about how to partition your logic into classes and avoid ending up with some monster classes (known as god class).
  • The Open-Close Principle (OCP): Modules should be open for extension and closed for modification. To implement a new feature, it is better to add a new derived class instead of having to modify some existing code.
  • The Liskov Substitution Principle (LSP): Methods that use references to base classes must be able to use objects of derived classes without knowing it. Array implements IList<T> but throws NotSupportException on IList<T>.Add(). This is a clear LSP violation.
  • The Interface Segregation Principle (ISP): The client should not depend by design on methods it does not use. This is why interfaces like IReadOnlyCollection<T> have been introduced. Often the client just needs a subset of features like read-only access instead of full read-write access.
  • The Dependency Inversion Principle (DIP): Depend on abstractions, not on implementations. Interfaces are much less subject to changes than classes, especially the ones that abide by the (ISP) and the (LSP).

The Dependency Inversion Principle Explained

The goal: an architecture resilient to changes

As for each SOLID principle, DIP is about system maintainability and reusability. Inevitably some parts of the system will evolve and will be modified. We want a design that is resilient to changes. To avoid that a change breaking too much, we must:

  • First, identify parts of the code that are change-prone.
  • Second, avoid dependencies toward those change-prone code modules.

Interfaces are stable

The Liskov Substitution Principle (LSP) and the Interface Segregation Principle (ISP) articles explain that interfaces must be carefully thought out. Both principles are the two faces of the same coin:

  • ISP is the client perspective: If an interface is too fat probably the client sees some behaviors it doesn’t care for.
  • LSP is the implementer perspective: If an interface is too fat probably a class that implements it won’t implement all its behaviors. Some behavior will end up throwing something like a NotSupportedException.

Efforts put in applying ISP and LSP result in interface stability. As a consequence, these well-designed interfaces are less subject to changes than concrete classes that implement them.

Also having stable interfaces results in improved reusability. We are pretty confident that the interface IDisposable will never change. Our classes can safely implement it and this interface is re-used all over the world.

First C# Example of the Dependency Inversion Principle

In this context, the DIP states that depending on stable interfaces is less risky than depending on change-prone implementations. DIP is about transforming this code:

into this code:

DIP is about removing dependencies from high-level code (like the ClientCode() method) to low-level code, low-level code being implementation details like the SqlConnection class. For that, we create interfaces like IDbConnection. Then both high-level code and low-level code depend on these interfaces. The key is that SqlConnection is not visible anymore from the ClientCode(). This way the client code won’t be impacted by implementation changes, like when replacing the SQL Server RDBMS implementation with MySql for example.

C# Example of the Dependency Inversion Principle

Now let’s illustrate the DIP with a real C# example. Here is usual C# code where a class Switch is used to operate a Lamp:

The main issue with this code is that the high-level class Switch is bound with the class Lamp. There are two disadvantages to that:

  • Switch cannot be used with something else than a Lamp. In the real world, a switch can work with any kind of electric device, an AirConditioner, a GarageDoor or a Television.
  • The class Lamp is an implementation. An implementation can change, it can go from 220 volts to 110 volts for example. When the class Lamp changes, it can break the class Switch.

Introducing Abstraction

By introducing an IDevice interface, both design issues are solved:

  • The class Switch can work with anything implementing the interface IDevice like an AirConditioner for example.
  • Internal details of the class Lamp can change. As long as a Lamp is an IDevice, the class Switch won’t be broken.

Benefits of applying the Dependency Inversion Principle

In the refactored code in the previous section we can see that:

  • Code is Lose-Coupled: Only the main method knows about what is switched on or off. Devices and Switches don’t know about each other.
  • Code is Resilient to Changes: The main method, the class Switch, Lamp and AirConditioner, all depend on the interface IDevice. Hopefully, this interface is stable. It has the single responsibility of making a device operatable.

As a side note, the class Lamp is an implementation details of an IDevice. Implementation details depend on the interface, not the opposite. Here is the meaning of the word Inversion in the DIP principle.

Making the Dependency Inversion Principle Actionable with the Level metric

Several code metrics can be used to measure, and thus constraint, the usage of DIP. One of these metrics is the Level metric. The Level metric is defined as follows:

From this diagram we can infer that:

  • The Level metric is not defined for components involved in a dependency cycle. As a consequence, null values can help track component dependency cycles.
  • The Level metric is defined for any dependency graph. Thus a Level metric can be defined for various granularity: methods, types, namespaces and projects.

DIP mostly states that types with Level 0 must be interfaces and enumerations. If we say that a component is a group of types (like a namespace or a project) the DIP states that components with Level 0 must contain mostly interfaces and enumerations. With a quick code query like this one you can have a glance at types Level and check if most of low level types are interfaces:

The Level metric can be used to track classes with a high-value for the Level metric. This is an indication that some interfaces must be introduced to break the long chain of concrete code calls:

The class Program has a Level of 8. If we look at the dependency graphs of types used from Program we can certainly see opportunities to introduce abstractions to be more DIP compliant:

Dependency Inversion Principle in Action

DIP versus Dependency Injection (DI)

The acronym DI is used for Dependency Injection. Since it is almost the same as the DIP acronym this leads to confusion. The I is used for Inversion or Injection which might add to confusion. Hopefully DI and DIP are very much related.

  • DIP states that classes that implement interfaces are not visible to the client code.
  • DI is about binding classes behind the interfaces consumed by client code.

DI means that some code, external to client code, configures which classes are used at runtime by the client code. This is simple DI:

Many .NET DI frameworks exist to offer flexibility in binding classes behind interfaces. Those frameworks are based on reflection and thus, they offer some kind of magic. The syntax looks like:

And then comes the Service Locator pattern. The client can use the locator to create instances of the concrete type without knowing it. It is like invoking a constructor on an interface:

Thus while DIP is about maintainable and reusable design, DI is about flexible design. Both are very much related. Let’s notice that the flexibility obtained from DI is especially useful for testing purposes. Being DIP compliant improves the testability of the code:

DIP versus Inversion of Control (IoC)

The Inversion word is used both in DIP and IoC acronyms. Here also this provokes confusion. Keep in mind that the word Inversion in the DIP acronym is about implementation details depending on interfaces, not the opposite. The Inversion word in the IoC acronym is about callback from Framework.

IoC is what differentiates a Framework from a Library. A library is typically a collection of functions and classes. On the other hand, a framework also offers reusable classes but also relies on callbacks. For example, UI frameworks offer many callback points through graphical events:

The method m_ButtonOnClick() is bound to the Button.OnClick event. It is a callback method. Instead of client code calling a framework method, the framework is responsible for calling back client code. This is an inversion in the control flow.

We can see that IoC is not directly related to DIP. However, both principles rely on abstractions to inverse the direction of a dependency.

  • DIP uses abstraction to achieve a lose-coupled design.
  • IoC uses abstraction as a means to call back.

Conclusion

As with other SOLID principles, the Dependency Inversion Principle is a key principle to wisely use the OOP abstraction and polymorphism concepts. This results in improving code maintainability, reusability and testability of your code.

This article concludes this SOLID posts series. Being aware of SOLID principles is not enough. When making design decisions one should take account of SOLID. But their usage must be constrained by the KISS principle: Keep It Simple Stupid. As explained in the post Are SOLID Principles Cargo Cult? it is easy to write spaghetti code in the name of SOLID principles. Then one can learn from experience. With years, identifying the right abstractions and partitioning properly the business needs in well-balanced classes is becoming natural.