Dependency Inversion in Practice

Szymon Tosik · February 26, 2024

Introduction

Upon recently re-reading Clean Architecture by Robert C. Martin I went through whole Part III where different design principles are discussed. Although I found all of them interestingly described when it comes to applicability in wider scope than usually they are discussed (mostly SOLID principles), I believe author put a lot of attention to one specific principle which stands for “D” in SOLID and is described as “Dependency Inversion Principle” (DIP shortly).

Standard definition of Dependency Inversion Principle states:

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details.

To simply put it, DIP encourages the use of abstractions (interfaces or abstract classes) to decouple high-level and low-level modules (classes, services, components) in a system, promoting a more flexible and extensible architecture.

Problem statement

Before jumping into the application of Dependency Inversion in practice, let’s first define the problem we would like to solve. One of the many forms we can observe violation of DIP is when we tightly couple high-level and low-level components, making impossible to change one without affecting the other.

Below we can observe direct DIP violation:

class BillingService {
    private val processor = PayPalPaymentProcessor()

    fun charge() {
        processor.process()
    }
}

class PayPalPaymentProcessor {
    fun process() {
        println("Processing PayPal payment")
    }
}

What went wrong here? The BillingService (high-level) class is tightly coupled with PayPalPaymentProcessor (low-level) class. High-level component depends on a concrete implementation of low-level component, every change in PayPalPaymentProcessor class will directly affect necessity of changes in BillingService class. Such structure not only makes future changes difficult but also makes testing harder. Can we do better? Yes, we can.

Above example can be illustrated with a simple dependency graph:

Before refactoring

Solution

Let’s refactor above example to see how it could be solved.

class BillingService(private val processor: PaymentProcessor) {
    
    fun charge() {
        processor.process()
    }
}

interface PaymentProcessor {
    fun process()
}

class PayPalPaymentProcessor: PaymentProcessor {
    fun process() {
        println("Processing PayPal payment")
    }
}

What just happened here? We introduced an interface PaymentProcessor which is implemented by PayPalPaymentProcessor class. We also changed BillingService class to accept PaymentProcessor as a constructor parameter. This way, we not only applied directly DIP but also using technique called Dependency Injection enabled the possibility to inject different implementations and interchangeability of PaymentProcessor implementation. Now, BillingService class is not dependent on a concrete implementation of payment processor, it’s dependent on an abstraction (interface), the same applies to PayPalPaymentProcessor class which by implementing PaymentProcessor interface is not dependent on BillingService class but the interface it implements.

After applying above changes, we can see that direction of our dependency graph has changed. Illustration below shows that both (low and high-level) components point to the abstraction (interface) instead of pointing to each other as before. We will expand direction of dependencies in the next section (Acyclic Dependency Principle).

After refactoring

Dependency Inversion in Architecture

The example provided above may not offer groundbreaking insights to many of us. Using interfaces as abstractions to decouple high-level and low-level components is a common practice. However, the real challenge arises when attempting to apply the Dependency Inversion Principle to higher-level concepts such as architecture. In his book Clean Architecture Robert C. Martin describes how the application of DIP can lead to the creation of more flexible and maintainable architecture. This is achieved by placing low-level infrastructure components behind a fine line of abstractions, thus keeping the core business logic independent of any external frameworks or libraries.

This particular approach to designing the architecture of our systems can be found in various sources under different names, such as Ports and Adapters, Hexagonal Architecture, and Clean Architecture. The underlying idea behind all of these approaches is the same, to keep the core business logic as the primary focus of the service, free from unnecessary coupling with external dependencies.

Meme

This architectural style applies the Dependency Inversion Principle as an additional restriction on the multiple layers of an application. As a result, all dependencies point towards the center, where the high-level component should reside. Therefore, the center is where we hope to find the domain model, the core functionality of the application. Achieving DIP in a layered architecture is done by creating abstract interfaces for the low-level details. These low-level details are typically referred to as adapters and sit at the boundary of your system. The abstractions are called ports and are part of the domain layer.

Hexagon

Acyclic Dependencies Principle

One of the few things which came to me as a surprise when reading Clean Architecture was the existence in literature of a principle such as Acyclic Dependencies Principle (ADP). The principle states:

allow no cycles in your component dependency graph

Robert C. Martin introduces two challenges when implementing systems:

  • morning after syndrome: code you were working on earlier doesn’t work in future because another developer changed a component elsewhere
  • the weekly build: it is common in medium-sized projects. Developers ignore each other for the first four days of the week. They all work on private copies of code and don’t worry about integrating their work on a collective basis. Then on the fifth day i.e on Friday, all of them integrate all their changes and build the system

It is also worth mentioning that keeping the structure of the components in form of acyclic direct graph (DAG) eliminates the possibility of encountering circular dependencies. This particular problem is usually interesting when it comes to building and deployment of particular components of the system. Having circular dependency between components causes multiple modules (units of deployment) to be built and later deployed even one of them has not changed.

If we added a dependency on the Auth module to the Entities, we would also impose a need to build the Interactors, which itself requires that Entities are build beforehand. Meaning that this specific module can not be developed in isolation anymore.

DAG

Can we prevent that? Of course.

Breaking the cycle

One of the most commons ways to deal with such situation is to introduce an abstraction (in form of interface) which we can use to reverse the dependency direction. Sounds familiar? Yes, it’s exactly what Dependency Inversion Principle is about.

DIP

By introducing IPermission interface, we reversed the direction of module dependency. What made possible to break the cycle in our dependency graph, and make Auth module independent of Entities. Now, they can be built and reason about separately.

Conclusions

Dependency Inversion Principle is a powerful tool that can be used to create more flexible and maintainable systems. By using abstractions to decouple high-level and low-level components, we can create solutions which are easily testable, loosely coupled and easy to maintain.

Twitter, Facebook