Dependency Inversion Principle in C# – SOLID Design Principles – Part 5

Posted on Updated on


Overview

In our introduction to the SOLID Design Principles, we mentioned the Dependency Inversion Principle as one of the five principles specified. In this post we are going to discuss this design principle. Though a discussion of Dependency Injection and Inversion of Control are warranted here, we will save those topics for another post. I want to discuss the Dependency Inversion principle as simply and directly as possible.

Dependency Inversion Principle – Introduction

The Dependency Inversion Principle is a software design principle that provides us a guideline for how to establish loose coupling with dependencies from higher-level objects to lower-level objects being based on abstractions and these abstractions being owned by the higher-level objects.

The definition reads as follows:

  1. High-level modules should not depend on low-level modules. They should both depend on abstractions.
  2. Abstractions should not depend upon details. Details should depend upon abstractions.

These two items state that when building software, classes should be loosely-coupled based on abstractions (interfaces) and the details of the implementation should be encapsulated within the concrete classes that contain the logic. From top-down perspective, higher-level classes have only loose dependencies with lower-level classes.

What does that mean?

Dependency Inversion is about reversing the standard and conventional direction of dependencies from higher-level objects to lower-level objects by ensuring that the interfaces that dictate the functionality of the lower-level objects (i.e. the contracts that mandate what the lower-level objects must do) are owned by the higher-level objects. After all, if we consider our software holistically, the higher-level orchestrating objects are more valuable than the lower-level objects because they are the objects whose functionality we are most likely going to have the desire or need to extend.

Because the interfaces (contracts) that define what but not how the lower-level objects function are owned by the higher-level consumers, the higher-level objects do not rely upon how a lower-level object provides its functionality. Instead, they define what they need the lower-level objects to do and the lower-level objects provide the functionality/behavior demanded.

What Problems Are Solved with Dependency Inversion?

Poorly written software that is difficult to maintain and that has too many “hard” dependencies is a nightmare! How many times have we seen objects with numerous dependencies that cause us to shy away from trying to make changes? In my career I have seen more than I would care to remember!

When proper design principles are not considered early in the process, we often end up with code that is a tangled web of dependencies and when we are forced to make changes we often encounter unforeseen breakages that occur because of these dependencies.

When we implement the Dependency Inversion Principle, we reduce the coupling between “pieces” of code. When we build dependencies based upon interfaces (contracts), we introduce greater stability over time and we reduce the “hard” dependencies from concrete class to concrete class. Thus we reduce the possibility of introducing instability over the course of time. By coding to interfaces, we force our implementing classes to honor their agreements while the objects that consume them have no need to know how they honor them.

When we build software components that are reusable and consumable and that have external dependencies, we can find great value in the Dependency Inversion Principle’s objectives.

A word of caution – we cannot just program to interfaces and say that we have implemented Dependency Inversion! I will admit that this is a great idea in and of itself but by itself it does not give us Dependency Inversion. To truly realize the benefits, we have to ensure that the interfaces we use are owned and controlled by the higher-level objects. In other words, the top-level consumers own the contracts that must be honored by the lower-level providers.

So, using interfaces alone is NOT Dependency Inversion! Using interfaces to define what is needed by higher-level objects and writing lower-level objects to provide those needs by honoring the needs of the consumers in a way that the consumers have no reason to know anything about how the lower-level objects do what they do is moving in the right direction.

It is reasonable to say that classes are always going to have dependencies on other classes. You just can’t have properly-built object-oriented software without having dependencies between classes. But when changes are needed, it is very advantageous to make sure that changes have minimal impact on the overall functionality of the classes with dependencies on the classes changed. When we have tight coupling between classes, it is difficult to do this.

Let’s consider two types that we all know – an Engine and a Starter. We can say that an Engine has a dependency on a Starter as shown in the diagram below.

Dependency Inversion Principle

If we assume here that every Engine has a Starter that’s a pretty reasonable assumption. We can say that when we instantiate an Engine object we must also create a Starter object to start the Engine. With this model, the two are tightly-coupled.

But what if we have to change the starter and replace it with a different one? What if we have to change something in the Starter class? Such a change could potentially break the functionality of the Engine. How can we prevent that?

First, let’s make a statement: an Engine requires a component that is able to start it. It does not specifically require a Starter. With this in mind we can begin to decouple the two. Look at the next design with the understanding that the dotted gray line signifies ownership and control of types:

EngineStarter2

We’ve created an IStarter interface and since the Starter class implements this we are in good shape, right? Not exactly. Yes, the Engine having a dependency on the IStarter interface is an improvement since any class that implements IStarter can be used but we don’t have a hard dependency on Starter. We are moving in the right direction but we’re not there yet!

Let’s think about this from the standpoint of Dependency Inversion. To invert the dependency, the higher-level object must define and control the interface. Think about this: what if the Starter object changed in a way that forced a change to the IStarter interface? That could potentially break the Engine class’s logic because although it only expects a type that implements IStarter, the IStarter interface does not belong to the Engine class and is not defined based on what it needs. Therefore, the perceived loose coupling here is not real because the higher-level object (Engine) does not truly have control over the IStarter interface.

To implement Dependency Inversion, the higher-level modules must have control over the interfaces that they use to define their requirements for lower-level objects. Let’s revisit our logical design this way:

Dependency Inversion Principle

Now we’re getting somewhere! The Engine class owns the IStarter interface and thus controls what the classes that implement IStarter must provide. Nice! With this scenario, a change to a class that implements the IStarter interface that changes the interface is not valid.

So where is the inversion? If we go back to the first Engine/Starter diagram above, we see that changes within that scenario bubble from the bottom up. Changes to lower-level components force changes to higher-level components. With our new design, since the higher-level component owns and controls the interface, any change in the lower-level component that breaks its interface implementation is not valid unless the higher-level object allows it and the IStarter interface is changed as well. If the Engine must be changed in a way that affects the IStarter interface, the forcing of change is top down. In other words, the Starter must be changed as a result of a mandated change by the Engine. Make sense?

Another Example of Dependency Inversion

Let’s look at a simple example to which we can all relate. Think about the electric appliances in your house. They all have voltage requirements. In the U.S. we generally have 110, 220 and in some cases 440-volt power. If you’ve ever noticed, the plugs for each voltage are different.

In the context of Dependency Inversion, we can think of an appliance as a higher-level consumer and of the power supply in our house as a lower-level object that provides functionality needed by the appliance to operate. The appliance defines and owns its defines its needs and publishes those needs via its physical plug “interface”.

Though this sounds overly simple, it is a valid example of Dependency Inversion. So when we have an appliance that requires 220-volt service and we plug it into a 220-volt outlet, we expect for it to just work. We know that the plug is capable of providing 220-volt power because (1) the plug on the wall matches the plug on the appliance, and (2) the receptacle and wiring were put in place by a licensed electrician who knows and understands the electrical codes and the functional requirements of any device that uses 220-volt power. In other words, they power supply honors the “contract” between it and any device or appliance manufactured to use it.

From a software development perspective, we can say that the appliance is the higher-level object, the power cord and plug are the contract that defines what is needed by the higher-level object, and that the power supply itself is the lower-level object that is capable of honoring the needs of the consumer. We know that it can provide the needs of the consumer because in this case the physical characteristics of the receptacle (wall plug) tell us that it “implements the proper interface”. We can even say that the plug/receptacle connection is an abstraction. The appliance has no need to understand how the receptacle provides 220-volt power just that it does.

Refactoring Our Original Engine/Starter Design Sample

Now that we have covered Dependency Inversion and considered a couple of simple examples, let’s now return to the Engine/Starter design to which we’ve been applying SOLID design principles over the past few posts.

To recap this design, way back in the first post on the Single Responsibility Principle (Part 1) we originally started with a simple design that represented an Engine and its dependency on a Starter to provide ignition functionality. Our first design looked like this:

Single Responsibility Principle - SOLID Design Principles - Improved design

Then, in the second post on the Open-Closed Principle (Part 2) we refactored the design to make it open to extension but closed to modification. In the original design, we identified the three types of starters to be electric, pneumatic, and hydraulic. Because the implementation details of each unique type of starter were so different, we moved from a single Starter type to distinct types for each. We then created an interface named IStarter that they all implemented.

Open Closed Principle

Due to the fact that each starter type had internal functionality that was so different, to ensure that our derived (implemented) classes being substitutable for their base types (or interfaces), we segregated our interfaces as we see below. You can refer back to these posts for more information – Liskov Substitution Principle (Part 3) and Interface Segregation Principle (Part 4).

Dependency Inversion Principle - Sample class layout

Our starting code looks like this:

namespace SOLIDDesignPrinciples
{
    #region Base Classes

    public class Component : IComponent
    {
        public string Brand
        {
            get;
            set;
        }

        public string Model
        {
            get;
            set;
        }
    }

    #endregion Base Classes

    #region Interfaces

    public interface IComponent
    {
        string Brand { get; set; }
        string Model { get; set; }
    }

    public interface IStarter : IComponent
    {
        IgnitionResult Start();
    }

    public interface IElectricStarter : IStarter
    {
        Battery Battery { get; set; }
    }

    public interface IPneumaticStarter : IStarter
    {
        AirCompressor Compressor { get; set; }
    }

    public interface IHydraulicStarter : IStarter
    {
        HydraulicPump Pump { get; set; }
    }

    #endregion Interfaces

    #region Starter Types

    public class ElectricStarter : Component, IElectricStarter
    {
        public Battery Battery
        {
            get;
            set;
        }

        public IgnitionResult Start()
        {
            //code here to initiate the electric starter
            return IgnitionResult.Success;
        }
    }

    public class PneumaticStarter : Component, IPneumaticStarter
    {
        public AirCompressor Compressor
        {
            get;
            set;
        }

        public IgnitionResult Start()
        {
            //code here to initiate the pneumatic starter
            return IgnitionResult.Success;
        }
    }

    public class HydraulicStarter : Component, IHydraulicStarter
    {
        public HydraulicPump Pump
        {
            get;
            set;
        }

        public IgnitionResult Start()
        {
            //code here to initiate the hydraulic starter
            return IgnitionResult.Success;
        }
    }

    #endregion Starter Types

    #region Starter Support Components

    public class Battery : Component
    {
        public bool IsCharged
        {
            get
            {
                /*we could write logic here to handle the
                  validation of the battery's charge
                  for now, we will just return true */
                return true;
            }
        }
    }

    public class AirCompressor : Component
    {
    }

    public class HydraulicPump : Component
    {
    }

    #endregion Starter Support Components

    #region Enums

    public enum IgnitionResult
    {
        Success,
        Failure
    }

    #endregion Enums
}

Basically, this code representation of the different types of Starter types required to fulfill the ignition functionality for an Engine type can be summarized as follows:

  • There are three distinct types of Starters – ElectricStarter, PneumaticStarter, and HydraulicStarter.
  • Each Starter type implements the IStarter interface which defines the properties that are common to all Starter types, and each implements its specific interface type. The specific interface type is necessary because each type of starter is unique in its own operational requirement(s).
  • The IStarter interface implements the IComponent interface.
  • Each Starter type inherits the Component base class and each Starter interface implements the IStarter interface.
  • The lower-level components required by the Starter types also inherit Component (since they are components too).

If we include the Engine itself in our dependency chain, we have a clear dependency hierarchy. Let’s look at it from an abstraction standpoint:

  • Engine has a dependency on a Starter, or rather anything that is an IStarter.
  • An IStarter can be an IElectricStarter, IPneumaticStarter, or IHydraulicStarter.
  • An IElectricStarter has a dependency on a Battery.
  • An IPneumaticStarter has a dependency on an AirCompressor.
  • An IHydraulicStarter has a dependency on a HydraulicPump.

Hold on a minute! Weren’t these supposed to all be abstractions? Yes they were, so let’s take a look at the lower-level objects and do some further decomposition.

If we are going to build dependencies based on abstractions all the way down the chain, we need to think a little more about what I will call the second-level objects. In the case of a Battery, in reality there are about five distinct types of automobile batteries! So to provide a proper abstraction from the ElectricStarter to a Battery, we must make this dependency an abstraction too.

If we refer to this resource,  we lean more about our previous statement that there are five distinct types of car batteries – just check it out if you are interested :). To be correct in our design and to make our code as closed to modification as possible, we must define each type as a distinct type within our design. These distinct types will then be set as dependencies of the ElectricStarter abstractly.

To do this, we first make the Battery class an abstract base class.

public abstract class Battery : Component, IBattery
    {
        public bool IsCharged
        {
            get;
            set;
        }

        public abstract void EvaluateCharge();
    }

We made the EvaluateCharge() method abstract to force our derived Battery classes to override its functionality.

We are also going to create specific battery type classes and although the actual logic behind how to validate the battery’s charge may vary significantly based on the type of battery, each type must still offer the EvaluateCharge() functionality. Make sense?

So here are our new classes for each type of battery:

public class WetFloodedBattery : Battery, IBattery
{
    public override void EvaluateCharge()
    {
        //logic here to evaluate the charge
        this.IsCharged = true;
    }
}

public class CalciumCalciumBattery : Battery, IBattery
{
    public override void EvaluateCharge()
    {
        //logic here to evaluate the charge
        this.IsCharged = true;
    }
}

public class VRLABattery : Battery, IBattery
{
    public override void EvaluateCharge()
    {
        //logic here to evaluate the charge
        this.IsCharged = true;
    }
}

public class DeepCycleBattery : Battery, IBattery
{
    public override void EvaluateCharge()
    {
        //logic here to evaluate the charge
        this.IsCharged = true;
    }
}

public class LithiumIonBattery : Battery, IBattery
{
    public override void EvaluateCharge()
    {
        //logic here to evaluate the charge
        this.IsCharged = true;
    }
}

If you look at these classes and say “they’re all the same!”, yes they are. Imagine that the charge validation logic is very different for each distinct type. That logic is not the point of this example, so we are not touching that. For this sample, all of the overridden methods just set the IsCharged property value to true 🙂 We have segregated each distinct type to honor the Single Responsibility Principle and give our classes only one reason to change. You can refer back to that post if you would like.

Next, to keep things simple, we have made our ElectricStarter class generic. When we create a new instance of an ElectricStarter we will have to inject an IBattery type into the constructor. This way we are using Constructor Injection to inject the ElectricStarter’s battery dependency upon creation. We will cover Dependency Injection in another post, so don’t get stuck on what we just said here. Just understand that we are requiring every instance of an ElectricStarter to be supplied with an instance of an IBattery type.

Let’s take a look at our new ElectricStarter class:

public class ElectricStarter<T> : Component, IElectricStarter<T>
    where T : IBattery
{
    public ElectricStarter(T battery)
    {
        this.Battery = battery;
    }

    public T Battery
    {
        get;
        set;
    }

    public IgnitionResult Start()
    {
        this.Battery.EvaluateCharge();
        if (this.Battery.IsCharged == true)
        {
            return IgnitionResult.Success;
        }
        else
        {
            return IgnitionResult.Failure;
        }
    }
}

We should also note that we have made our IElectricStarter interface generic.

public interface IElectricStarter<T> : IStarter
{
    T Battery { get; set; }
}

So far so good. Now let’s return to our Engine class and make some changes.

First, we will make the Engine.Start() accept the IStarter instance.

public interface IEngine : IComponent
{
    IgnitionResult Start(IStarter starter);
}

#endregion Interfaces

#region Engine

public class Engine : Component, IEngine
{
    public IgnitionResult Start(IStarter starter)
    {
        return starter.Start();
    }
}

#endregion Engine

If we then flip over to the Program.cs class of our sample C# Console application, and add code to instantiate an Engine, a starter, and a battery and run it we will see a successful engine start!

namespace SOLIDPrincipleSamples
{
    class Program
    {
        static void Main(string[] args)
        {
            IEngine engine = new Engine();

            IBattery battery = new LithiumIonBattery();
            IElectricStarter<IBattery> starter = new ElectricStarter<IBattery>(battery);

            IgnitionResult result = engine.Start(starter);

        }
    }
}

So now that we have defined this design, are working with dependencies based on abstractions, and have an Engine that we can start, let’s consider the ownership of the interfaces we have defined from a Dependency Inversion standpoint.

First, remember that the higher-level objects must own the interfaces. If we consider our model from the top down, we start with the Engine.

The Engine depends on a Starter, or more appropriately an IStarter. Since we created interfaces for each of the three types of starters (IElectricStarter, IPneumaticStarter, IHydraulicStarter) the Engine should own and have control over those interfaces.

Each type of Starter has a dependency on some lower-level object. In the case of the ElectricStarter, there is a dependency on a Battery, or should we say an IBattery. As we built our code we discovered that there are actually five different kinds of car batteries and we created classes for each and each class implemented the IBattery interface. Therefore, the ElectricStarter should own the IBattery interface. We could go so far as to say that the Engine could conceivably own it as well and that would not necessarily be wrong, but we want to segregate ownership where it truly belongs.

If you want to create your own C# console application and play with this a little more, the code for the Engine, starters, and all other classes/interfaces is below. You can copy and paste this into a single .cs file if you want and experiment.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SOLIDDesignPrinciples
{
    #region Base Classes

    public class Component : IComponent
    {
        public string Brand
        {
            get;
            set;
        }

        public string Model
        {
            get;
            set;
        }
    }

    public abstract class Battery : Component, IBattery
    {
        public bool IsCharged
        {
            get;
            set;
        }

        public abstract void EvaluateCharge();
    }

    #endregion Base Classes

    #region Interfaces

    public interface IComponent
    {
        string Brand { get; set; }
        string Model { get; set; }
    }

    public interface IStarter : IComponent
    {
        IgnitionResult Start();
    }

    public interface IElectricStarter<T> : IStarter
    {
        T Battery { get; set; }
    }

    public interface IPneumaticStarter : IStarter
    {
        AirCompressor Compressor { get; set; }
    }

    public interface IHydraulicStarter : IStarter
    {
        HydraulicPump Pump { get; set; }
    }

    public interface IBattery : IComponent
    {
        bool IsCharged { get; set; }
        void EvaluateCharge();
    }

    public interface IEngine : IComponent
    {
        IgnitionResult Start(IStarter starter);
    }

    #endregion Interfaces

    #region Engine

    public class Engine : Component, IEngine
    {
        public IgnitionResult Start(IStarter starter)
        {
            return starter.Start();
        }
    }

    #endregion Engine

    #region Starter Types

    public class ElectricStarter<T> : Component, IElectricStarter<T>
        where T : IBattery
    {
        public ElectricStarter(T battery)
        {
            this.Battery = battery;
        }

        public T Battery
        {
            get;
            set;
        }

        public IgnitionResult Start()
        {
            this.Battery.EvaluateCharge();
            if (this.Battery.IsCharged == true)
            {
                return IgnitionResult.Success;
            }
            else
            {
                return IgnitionResult.Failure;
            }
        }
    }

    public class PneumaticStarter : Component, IPneumaticStarter
    {
        public AirCompressor Compressor
        {
            get;
            set;
        }

        public IgnitionResult Start()
        {
            //code here to initiate the pneumatic starter
            return IgnitionResult.Success;
        }
    }

    public class HydraulicStarter : Component, IHydraulicStarter
    {
        public HydraulicPump Pump
        {
            get;
            set;
        }

        public IgnitionResult Start()
        {
            //code here to initiate the hydraulic starter
            return IgnitionResult.Success;
        }
    }

    #endregion Starter Types

    #region Starter Support Components

    public class WetFloodedBattery : Battery, IBattery
    {
        public override void EvaluateCharge()
        {
            //logic here to evaluate the charge
            this.IsCharged = true;
        }
    }

    public class CalciumCalciumBattery : Battery, IBattery
    {
        public override void EvaluateCharge()
        {
            //logic here to evaluate the charge
            this.IsCharged = true;
        }
    }

    public class VRLABattery : Battery, IBattery
    {
        public override void EvaluateCharge()
        {
            //logic here to evaluate the charge
            this.IsCharged = true;
        }
    }

    public class DeepCycleBattery : Battery, IBattery
    {
        public override void EvaluateCharge()
        {
            //logic here to evaluate the charge
            this.IsCharged = true;
        }
    }

    public class LithiumIonBattery : Battery, IBattery
    {
        public override void EvaluateCharge()
        {
            //logic here to evaluate the charge
            this.IsCharged = true;
        }
    }

    public class AirCompressor : Component
    {
    }

    public class HydraulicPump : Component
    {
    }

    #endregion Starter Support Components

    #region Enums

    public enum IgnitionResult
    {
        Success,
        Failure
    }

    #endregion Enums
}

Points to Ponder

One more thing to think about with Dependency Inversion is how abstraction is handled within the various layers of an application or object hierarchy. I have written numerous application frameworks in my time as an architect and developer, and one thing that holds true when writing this type of code is that you find yourself within the various levels writing abstract code with the intent of it being used concretely at some later point in time. It is necessary to rely upon generic abstractions within lower levels and allowing the ultimate consumers/clients to determine the exact implementation details. With Dependency Inversion, we can effectively build objects that perform the necessary tasks without knowing the exact details of each task at the time we write them. This idea may sound a bit counterintuitive at first, but it is something to think about as you explore this topic more deeply.

Additional Links

The SOLID Design Principles
The Single Responsibility Principle (SRP)
The Open-Closed Principle (OCP)
The Liskov Substitution Principle (LSP)
The Interface Segregation Principle (ISP)
The Dependency Inversion Principle (DIP)

6 thoughts on “Dependency Inversion Principle in C# – SOLID Design Principles – Part 5

    Nero theZero said:
    July 30, 2016 at 12:09 am

    Forgive me if I’m being stupid. I’ve copied all the code and pasted in a single .cs file, but I didn’t get how do I give the ownership of the interfaces to a class? For example, how do I give the ownership of the IStarter interfaces to the Engine class?

    Like

      johnnels responded:
      July 31, 2016 at 5:51 pm

      That is actually a really good question, and in reality, the ownership is conceptual. Generally, this could be implemented by placing the interfaces that define what the higher-level classes require in either the same assembly with the higher-level classes, or in another assembly entirely.

      Like

    Nero theZero said:
    July 30, 2016 at 12:20 am

    What I understand, I need to put the high-level and low-level classes in separate assemblies. Right?

    Like

    Nero theZero said:
    July 31, 2016 at 7:04 pm

    Thanks for your reply 🙂

    Like

      johnnels responded:
      August 9, 2016 at 7:54 pm

      You are very welcome, and thank you for taking the time to read and dig in 🙂 Don’t hesitate to reach out any time!

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s