SOLID part 5: The Dependency Inversion Principle

Posted by : on

Category : Architecture

This post is part of an eight post series about the SOLID principles and code architecture. You can find the other posts here:

Introduction: What is Dependency Inversion

The last of the SOLID principles is the Dependency Inversion Principle. Before we get to the specifics of the DIP, let’s talk about what is Dependency Inversion.

If we have a class A that depends on a class B in our source code then A depends on B at compile time. If A calls a method or uses a field of B then it also depends on B at runtime. Dependency Inversion is when the compile time and runtime dependencies are inverted.

That doesn’t mean that whenever we invert a dependency between two classes, we apply Dependency Inversion. Here is an example:

public class A
{
   private readonly B _b;
   
   public A(B b) => _b = b;

   public void AMethodOfA()
   {
      Console.WriteLine("Inside method of A");
      _b.AMethodOfB();
   }
}

public class B
{
   public void AMethodOfB() => Console.WriteLine("Inside method of B");
}

Here we can see that class A Depends on class B at compile time, because it needs to know an instance of B. It also depends on B at runtime, because it calls at runtime a method of B. Let’s try to reverse the dependency between compile time and runtime:

public class A
{
   public Action? bMethod;
   
   public void AMethodOfA()
   {
      Console.WriteLine("Inside method of A");
      bMethod?.Invoke();
   }
}

public class B
{
   public B(A a) => a.bMethod += AMethodOfB;

   public void AMethodOfB() => Console.WriteLine("Inside method of B");
}

Here at runtime, the class A will still call a method of B, but at compile time class B depends on A.

Having two classes A and B and inverting the dependency between them is not dependency inversion, in our previous example if we would refactor from the second way to the first we would have inverted the dependencies between our classes, but that would not be a dependency inversion.

Dependency inversion exists when the dependencies between runtime and compile time are inverted.

What Is The Dependency Inversion Principle

The DIP tells us that we should apply Dependency Inversion through abstractions. Historically has been defined like this:

Depend upon Abstractions. Do not depend upon concretions.

Or in a more detailed way:

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

Before we get to the reason why we should be using DIP, we should realize that the above is not true only for composition. Inheritance from a base class that is not abstract, depends on a concrete implementation and that also violates the DIP.

A child class, depends on the implementations that exist in its parent class. Even if the parent class has virtual methods that are implemented in the child class, both classes are concrete, none is abstract and this violates the DIP.

Why to use the Dependency Inversion Principle

Traditionally a program, has some high level policies like the business rules, that have to call lower level policies that are implementation details, for example the UI, a Database, or the input method.

Those high level policies are less likely to change than the low level details. We don’t want our changes in the UI for example to create the need to change our code in classes that are concerned with the business rules. A dependency inversion, so that our UI is dependent on the classes that implement the business rules is beneficial, because any changes to the UI, which are more likely to happen as it is a low level policy, won’t force any change to our higher level modules.

The Dependency Inversion Principle takes this one step further. Concrete implementations change more often than abstractions. Abstractions describe what an object can do and not how it does it. Because we want to minimize the amount of changes we have to do in our code, every time something needs to change, both high and low level policies which are concrete implementations should depend on abstractions and our abstractions should not depend on concrete implementations but on other abstractions. Anything concrete is more volatile than anything abstract.

Some Concrete Dependencies can be ignored

Some Concrete dependencies can safely be ignored, and we never have to think if we should apply the DIP on those.

For example in C# the dependency upon concrete classes that exist in the BCL, won’t create a problem, as these classes are not volatile and breaking changes on those classes is not expected.

On the other hand any class that we have written should be considered volatile, also any external code that we decide to use and is an implementation detail, for example which database our program will use, or which external libraries and frameworks, should also be considered volatile.

This also, highlights another benefit of the DIP, different from the resistance to big changes due to volatility of our concrete implementations:

Defer decisions to the future

By using the DIP, we can postpone decisions we have to make about our implementation details. Hiding a low level detail behind an abstraction, allows us to code our high level policies without the need to know anything about that detail.

For example, we can create our code in a way that depends on an abstraction that loads data. This way, we can load data to test our code without the need to have a database. We can load our data from a simple text or json file and when we are pleased with the implementation of our high level policies, create an adapter that implements our abstraction and uses the database we need.

Factories for concrete dependencies

Not to depend upon concrete implementations at all may sound nice, but it is not realistic. After all, eventually we need to create objects from our types. The best way to deal with the creation of our objects, is to minimize the impact the changes in the concrete implementations will have to our code base.

This is where factories are useful. If any changes are needed, they are concentrated in a specific place. As a simple example, if we have a game that has different enemies in different territories, instead of creating those enemies in the code that implements each territory, we can have something like this:

public class EnemyFactory : IEnemyFactory
{
   public IEnemy GetGraveyardEnemy() => new Skeleton();
   public IEnemy GetPlainsEnemy() => new Dragon();
   public IEnemy GetCastleEnemy() => new Goblin();
}

Any change to the type of enemy each location has, can be done in a single file, in a single line. We don’t have to go looking inside our whole codebase for statements that depend on the enemies concrete implementations.

Relation To The Open Closed Principle

Being dependent upon abstractions and the benefits this has, may remind us of the Open Closed Principle. The DIP and the OCP are closely related. The OCP describes what we want to achieve and the DIP describes a mechanism to achieve it.

By following the DIP, we have a way to make changes to the behavior of our program, without changing our code. Because our code depends on abstractions, we can modify the behavior of a class by extending our code with new implementations.

In the above enemies example, if we needed a new type of enemy for our graveyard, let’s say a Zombie, we would create a zombie class, return it from the GetGraveyardEnemy method, but no changes to the code that is responsible for our graveyard behavior would be needed, as this code would depend on the IEnemies interface and not on any concrete implementation of the Skeleton class.

An example of the Dependency Inversion Principle

Let’s see an example in code, for the DIP.

Suppose we have enemies that whenever they attack, a light in our UI flashes red. We could have done something like this:

public class Enemy
{
   private readonly UILight _uiLight;

   public Enemy(UILight uiLight)
   {
      _uiLight = uiLight;
   }
   
   public void Attack(Character character)
   {
      // Code for Attacking
      _uiLight.Flash(Color.Red);
   }
}

public class UILight
{
   public void Flash(Color color)
   {
      // Code fore flashing the Light
   }
}

Here a high level module (the Enemy) depends on a low level module (the UI light).

The DIP tells us that both should depend on abstractions. But what abstraction should that be? Someone could have done this:

public class Enemy
{
   private readonly IUILight _uiLight;
   
   public Enemy(IUILight uiLight)
   {
      _uiLight = uiLight;
   }
   
   public void Attack(Character character)
   {
      // Code for Attacking
      _uiLight.Flash(Color.Red);
   }
}

public interface IUILight
{
   void Flash(Color color);
}

public class UILight : IUILight
{
   public void Flash(Color color)
   {
      // Code fore flashing the Light
   }
}

Although typically this conforms to the DIP, it is not correct. As we saw in The conceptual meaning of Interfaces an interface conceptually belongs to the class that is using it, not the one that is implementing it.

Our Enemy class has no reason to know that is communicating with a UI light, and it doesn’t have a reason to know that this light should be flashing red. The only thing it needs to know is that it communicates with a class that can give a notification that the enemy is attacking, something like this:

public class Enemy
{
   private readonly IUIEnemyNotification _iuiEnemyNotification;
   
   public Enemy(IUIEnemyNotification iuiEnemyNotification)
   {
      _iuiEnemyNotification = iuiEnemyNotification;
   }
   
   public void Attack(Character character)
   {
      // Code for Attacking
      _iuiEnemyNotification.EnemyAttack();
   }
}

public interface IUIEnemyNotification
{
   void EnemyAttack();
}

public class UILight : IUIEnemyNotification
{
   public void EnemyAttack()
   {
      Flash(Color.Red);
   }
   
   private void Flash(Color color)
   {
      // Code fore flashing the Light
   }
}

Why this is helpful

Any change to our UI, which is a low level module and that means it is more likely to change, can now be done without affecting our other classes: The concrete implementation or our abstraction. In fact, the Enemy class with the IUIEnemyNotification interface could be in their own assembly, and any UI related class that implements the IUIEnemyNotification in a separate one.

This makes any changes easier to implement. If for example later, the requirements change and instead of a flashing light we decide that whenever an enemy attacks, a counter increases that shows the number of enemy attacks that have happened, the only thing we have to do, is to create a new counter class that implements the IUIEnemyNotification and calls inside the EnemyAttack method its own Counter.Increase method.

If we have a factory for our concrete dependencies, as shown previously, no one has to touch the Enemy class. In fact two different people could work on the code, one on the Enemy class and another on whatever class is responsible for the UI notifications for the enemy and the only restriction between them is that the one that works in the UI should follow the ‘contract’ which is defined by the IUIEnemyNotification abstraction. Any changes to the UI class for notifications will have no effect on the Enemy class. This is why the DIP, is a mechanism that allows us to follow the Open Closed Principle.

Static polymorphism

Dynamic polymorphism is not the only way to conform to the DIP. Using generics, we can also use static polymorphism. Static polymorphism has less overhead because it is resolved at compile time but also has less flexibility because it cannot change at runtime.

The static polymorphism breaks our source code dependency but because it is resolved at compile time, the generic class has to be recompiled any time we make changes. Generally unless the small gains we get in execution speed from static polymorphism are needed, dynamic polymorphism is preferred.

Conclusion

This was the last of the SOLID principles: The Dependency Inversion Principle. As with the other SOLID principles, overdoing it can create more problems than it solves. In the next post which will be the last post about SOLID, I want to talk about maybe the most important think in SOLID which is not the principles that is composed of.

In the next post, we’ll see why SOLID is important in general, how, when and where it should be used, what it means to over do it with the application of the SOLID principles, how they can affect our architecture positively and when they should not be used as a good investment of our time. Because as I mentioned in my Software Architecture in Game Development post, software architecture is about time investment.

Thank you for reading and as always, if you have any questions or comments you can use the comments section or contact me directly via the contact form or by email. Also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.


About Giannis Akritidis

Hi, I am Giannis Akritidis. Programmer and Unity developer.

Follow @meredoth
Follow me: