SOLID Principles Made Ridiculously Simple: Dependency Inversion Principle

Welcome back! After a long hiatus, I have decided to complete this series with the final principle (and hopefully from now on will consistently post more stuff about software engineering and machine learning in)

Today, we introduce the last of the SOLID principles, the Dependency Inversion Principle.

Major Monogram needs to manage many agents, starting from Agent A all the way to Agent Z (Agent P included). For this purpose, he has tasked an AgentManager to manage the agents. Specifically, let’s just focus on the part where the AgentManager manages Agent P, (which is actually Perry the Platypus).

Major Monogram tirelessly managing all the agents
class AgentManager {
    AgentP perryThePlatypus;

    void reconnaissance() {
        perryThePlatypus.disguise();
    } 

    void recall() {
        perryThePlatypus.teleportBack();
    }
}

Today, Phineas and Ferb decided to bring perry along on an excursion so perry is off duty.

The AgentManager has to manage another agent, Agent D just for the day. However, Agent D’s method of reconnaissance and recalling is different from Agent P!

class AgentManager {
    AgentD dogAgent;

    void reconnaissance() {
        dogAgent.sneak();
    } 

    void recall() {
        dogAgent.run();
    }
}

Just look at all the code changes in the AgentManager just to manage another Agent! This is because the AgentManager depends on AgentP previously. We want to invert the dependency to an abstraction.

What does this mean?

An abstraction, in code terms, could be the use of an interface. Let’s explore!

interface Agent {
    void reconnaissance();

    void recall();
}

class AgentP implements Agent {
    @Override
    void reconnaissance() {
        this.disguise();
    }

    @Override
    void recall() {
        this.teleportBack();
    }
    
    // implementations to disguise and telportBack omitted
}

class AgentD implements Agent {
    @Override
    void reconnaissance() {
        this.sneak();
    }

    @Override
    void recall() {
        this.run();
    }

    // implementations to sneak and run omitted
}

And then, our AgentManager will look like this:

class AgentManager {
    Agent agent;

    void reconnaissance() {
        agent.reconnaissance();
    } 

    void recall() {
        agent.recall();
    }
}

This design change to AgentManager will make it resilient to changes. As long as any new Agent conforms to the Agent interface, the AgentManager will be able to handle it with zero code changes!

Why is this known as dependency inversion, you may ask? Well, now the AgentManager depends only on the Agent interface (which is an abstraction), and the respective agents AgentP and AgentD (or any of the 24 other agents) now also depend on the Agent interface!

Lose Coupling

The idea of dependency inversion is to ensure entities are loosely coupled so that they will be more resilient to code changes and will not affect each other as much as they should. There is a misconception that dependency inversion inverts the dependency so that low-level modules depend on high-level modules; this is not true. Rather, both the low and high level modules now depend on an abstraction.

How to follow the Dependency Inversion Principle

Whenever a class contains (or depends on) an object, consider if you can provide an interface for the object to implement, then let the class contain the interface instead. You may think that it is troublesome to create lots of interfaces at the beginning, but all the preparation work done now is to allow our code to scale better and be more resilient to changes.

That’s it! Let me think about what I should write about next!

Comments are closed.