Severin Perez

Effective Program Structuring with the Dependency Inversion Principle

October 11, 2018

In the previous four parts of the SOLID series we discussed how to compose object-oriented code that is flexible, maintainable, and reusable. Achieving this goal requires careful attention to how a particular entity (class, module, function, object, etc.) does its work. A significant consideration in this regard is that of dependencies. Does your entity depend on another entity to do its work? If so, how tightly coupled are the two? Will changes in one cascade into the other? These are important questions, which we discussed in part when reviewing the Open/Closed Principle and the Liskov Substitution Principle; however, dependency organization is an issue that warrants closer examination. That’s where the final SOLID principle comes in: the Dependency Inversion Principle (DIP).

A Quick Refresher on SOLID

SOLID is an acronym for a set of five software development principles, which if followed, are intended to help developers create flexible and clean code. The five principles are:

  1. The Single Responsibility Principle—Classes should have a single responsibility and thus only a single reason to change.
  2. The Open/Closed Principle—Classes and other entities should be open for extension but closed for modification.
  3. The Liskov Substitution Principle—Objects should be replaceable by their subtypes.
  4. The Interface Segregation Principle—Interfaces should be client specific rather than general.
  5. The Dependency Inversion Principle—Depend on abstractions rather than concretions.

The Dependency Inversion Principle

At its heart, the DIP is about structure. The manner in which you structure your program entities, and the way in which they interact with one another, has a direct impact on your ability to conform to the rest of the SOLID principles (the benefits of which we have discussed previously.) If your dependencies are mismanaged, then the likelihood of your code being flexible, maintainable, and reusable is drastically diminished.

In his paper on the DIP, Robert C. Martin enumerates the primary characteristics of poorly-designed software as follows: it is rigid, meaning that it is hard to change due to the cascading effects of changes in one place into another; it is fragile, meaning that changes result in unexpected breakage; and, it is immobile, in that you cannot reuse entities due to their entanglement with one another. [1] Martin attributes these problems primarily to poor structural design. As entities become more tightly coupled to one another, their rigidity, fragility, and immobility increase. In other words, dependency management is the key to writing software that is more flexible and therefore easier to maintain and reuse.

If the problem with bad software is related to dependency structure, then what is the solution? It is here that Martin flips the classic dependency structure on its head and argues for dependency inversion. In traditional entity layering, higher level entities depend on lower level entities, which in turn depend on even lower level entities. This is a typical top-down structure wherein entities that perform work at the policy level will delegate behavior down an increasingly detail-focused chain of dependencies. The problem with this model is that changes at the lower level can force changes at the higher level, which in turn makes reuse of higher level entities very difficult. Compare this to an “inverted” dependency structure wherein both high-level and low-level entities depend on shared abstractions. Here, different layers within a software program form a contract with one another to use a shared abstraction when they interact. In doing so, the layers free themselves to implement details however they may choose without concern for affecting one another.

Put concisely, the DIP says that high- and low-level modules should depend on mutual abstractions, and furthermore, that details should depend on abstractions rather than vice versa. By implementing a dependency structure that follows this principle, you can free your modules from one another in a way that opens them up for reuse. So long as an entity conforms to the prescribed contract of its abstraction dependencies, it can be used anywhere.

A Failure to Abstract

Of course, as with any principle that is meant to be applied to real-world work, the best way to understand it is through examples and practice. Let’s start by looking at a program that could benefit from better use of abstractions.

using System;

namespace dip_1
{
    class Program
    {
        static void Main(string[] args)
        {
            var bakery = new Restaurant("Bakery");
            bakery.Cook("cookies");
        }
    }

    class Restaurant
    {
        public string Name { get; set; }

        private Oven Oven = new Oven();

        public Restaurant(string name)
        {
            this.Name = name;
        }

        public void Cook(string item)
        {
            this.Oven.LightGas();
            this.Oven.Bake(item);
            this.Oven.ExtinguishGas();
        }
    }

    class Oven
    {
        public bool On { get; set; }

        public void LightGas()
        {
            Console.WriteLine("Lighting the oven's gas!");
            this.On = true;
        }

        public void ExtinguishGas()
        {
            Console.WriteLine("Extinguishing the oven's gas!");
            this.On = false;
        }

        public void Bake(string item)
        {
            if (!this.On)
            {
                Console.WriteLine("Oven's gas is not turned on.");
            }
            else
            {
                Console.WriteLine("Now baking " + item + "!");
            }
        }
    }
}

Here we have an perfectly reasonable first attempt at writing a program that governs the activities of a restaurant. For brevity’s sake, our restaurant can’t do much, but you can see how the program might be expanded to include other functionality. Currently, our Restaurant class has an Oven member and a method called Cook, which calls the Oven object’s three methods: LightGas; Bake; and, ExtinguishGas. Indeed, when this program executes we are able to successfully instantiate a Restaurant, name it “Bakery”, and use it to make some delicious cookies.

So, if everything in this program works, what’s the problem? Consider the following:

  • The Restaurant class depends on usage of its Oven object. What if we wanted to make a restaurant that uses a different kind of cooking instrument? As currently implemented, we couldn’t do so without going into the Restaurant class and making changes, which would violate the Open/Closed Principle.
  • Changes to the Oven object have the potential to cascade through the program and break the Restaurant class’ Cook method. For example, what if we decided that we wanted to make our ovens electric rather than gas and changed the LightGas and ExtinguishGas method names? Doing so would effectively break Restaurant because it relies on using those Oven methods as currently named.
  • The coupling between Restaurant and Oven reduces portability, meaning that we can’t re-use Restaurant in another location without bringing Oven with it. (Even if the other program never uses Oven.)

The above is an example of a program that works, but is poorly designed. It is rigid, fragile, and immobile. Certainly we can do better.

Abstraction and Inversion

Our sample program leaves a lot of room for improvement. As a first step to designing a better dependency structure, we should consider what kind of abstractions actually exist here. Our Restaurant uses an Oven as part of the work accomplished in its Cook method. This is a great clue—the intent actually has nothing to do with the Oven, rather, the intent is to cook more generally. Surely our chef knows that there are other ways to cook food than with an oven, so let’s give her more choices by abstracting away the idea of a device that cooks something.

using System;

namespace dip_2
{
    class Program
    {
        static void Main(string[] args)
        {
            var bakery = new Restaurant("Bakery", new Oven());
            bakery.Cook("cookies");

            var crepery = new Restaurant("Crepery", new Stove());
            crepery.Cook("crepes");
        }
    }

    class Restaurant
    {
        public string Name { get; set; }

        public ICooker Cooker { get; set; }

        public Restaurant(string name, ICooker cooker)
        {
            this.Name = name;
            this.Cooker = cooker;
        }
        
        public void Cook(string item)
        {
            this.Cooker.TurnOn();
            this.Cooker.Cook(item);
            this.Cooker.TurnOff();
        }
    }

    interface ICooker
    {
        bool On { get; set; }

        void TurnOn();

        void TurnOff();

        void Cook(string item);
    }

    class Oven : ICooker
    {
        public bool On { get; set; }

        public void TurnOn()
        {
            Console.WriteLine("Turning on the oven!");
            this.On = true;
        }

        public void TurnOff()
        {
            Console.WriteLine("Turning off the oven!");
            this.On = false;
        }

        public void Cook(string item)
        {
            if (!this.On)
            {
                Console.WriteLine("Oven not turned on.");
            }
            else
            {
                Console.WriteLine("Now baking " + item + "!");
            }
        }
    }

    class Stove : ICooker
    {
        public bool On { get; set; }

        public void TurnOn()
        {
            Console.WriteLine("Turning on the stove!");
            this.On = true;
        }

        public void TurnOff()
        {
            Console.WriteLine("Turning off the stove!");
            this.On = false;
        }

        public void Cook(string item)
        {
            if (!this.On)
            {
                Console.WriteLine("Stove not turned on.");
            }
            else
            {
                Console.WriteLine("Now frying " + item + "!");
            }
        }
    }
}

In this version of the program we created an abstraction called ICooker, which will serve as the mutual contract between our Restaurant and any cooking device we care to give it. Instead of hard-coding a particular cooking device into Restaurant, we pass it one at instantiation, which it then uses in its Cook method. On the other side of the equation, our Oven class now implements the ICooker interface, which lets it fill the roll of a general “cooker” device. And to prove that other devices can fill the same roll, we also have a Stove class that does the same. When this program executes, we are able to use the same Restaurant class to instantiate two different restaurants, one with an Oven and one with a Stove. Both of them operate as expected and produce cookies and crepes respectively.

If you compare this version to our initial attempt, you’ll see a number of structural improvements.

  • The Restaurant class no longer depends on the Oven class, meaning that we can create and use as many restaurants with as many different cooking instruments as we like, so long as they all abide by the contract defined in the ICooker interface.
  • The Oven class is generalized such that it implements the TurnOn, TurnOff, and Cook methods prescribed in the ICooker interface. As a result, we can make changes in the Oven class (for example, defining whether it is a gas or an electric oven) without affecting the Restaurant class.
  • All of our classes have increased portability because they are loosely coupled. Each depends on the ICooker interface, but that is an abstraction that we can easily carry to other programs without having to bring implementation details with it.

Surely there is still room for improvement in this short program (perhaps implementation of an IKitchen interface that could also be used in home kitchens and food trucks…); however, thanks to the DIP, it is a clear improvement over the first version when it comes to flexibility, maintainability, and reusability.

TL;DR

The final SOLID principle of software development is the Dependency Inversion Principle (DIP), which says that both high- and low-level modules should depend on mutual abstractions rather than directly on one another. Use of the DIP improves program flexibility and maintainability because it sets up dependencies in a way that decreases the effect that changes in one place have on another. Furthermore, the DIP increases the degree to which an entity from one program can be reused in another. In order to adhere to the DIP, the first step is to understand the abstractions in your program and then use them to build contracts between your entities rather than hard-code implementation details.

References


Note: This article was originally published on Medium.


You might enjoy...


© Severin Perez, 2021