Skip to main content

Command Palette

Search for a command to run...

SOLID Principles: Dependency Inversion (DI)

Published
3 min readView as Markdown
M

Building Pyramids of Code: A Tech Leader from Cairo

I hail from the land of the Great Pyramids, where I've traded ancient wonders for the ever-evolving marvels of software. As Head of Software Engineering, my days are a captivating blend of the Nile's serenity and the frenetic energy of building cutting-edge tech solutions.

Cairo's vibrant tech scene is my playground. I've navigated its bustling hubs, honed my skills at startups and consultancies, and witnessed firsthand the power of technology to transform lives. This journey has instilled in me a deep understanding of the region's unique needs and opportunities.

My leadership philosophy? Think straight-talking Sphinx – I guide with unwavering honesty, challenging directly while caring fiercely. No sugarcoating, no backstabbing, just clear feedback and genuine support. We build pyramids of code, not walls of BS.

My team thrives in this arena of radical candor. We rip through roadblocks with open communication, celebrate wins loudly, and learn from failures even faster. It's messy, sometimes brutal, but always authentic. Because in the crucible of challenging directly, true brilliance emerges. I empower my teams to unleash their creativity, fostering a culture of innovation where ideas flow like the Nile. We tackle complex challenges, from scaling platforms to crafting intuitive interfaces, all while embracing the dynamic spirit of Cairo.

Introduction

Introduction

Imagine working on a complex software project where changes in one small corner trigger a domino effect of adjustments throughout the entire system. This tight coupling between components can be a nightmare for developers, hindering maintainability and flexibility.

The Dependency Inversion Principle (DIP) is a powerful tool in object-oriented design that tackles this very issue. It promotes a shift in how components rely on each other, leading to loose coupling, increased flexibility, and improved testability.

Key Ideas

  • High-level modules (business logic) depend on abstractions (interfaces or abstract classes) rather than specific implementations (concrete classes).

  • Low-level modules (utility functions, data access) implement these abstractions.

  • Loose Coupling: Components become more independent, with changes in one part having minimal impact on others.

  • Increased Flexibility: The system becomes more adaptable, allowing you to swap out components or introduce new functionalities with ease.

  • Improved Testability: Loose coupling makes it simpler to isolate and test individual components.

Unclean Example (Not Following) DI

Imagine a NotificationService class that sends notifications. In a non-ideal scenario, it might directly access a concrete EmailSender class for sending emails.

public class NotificationService {

  private EmailSender emailSender;

  public NotificationService() {
    this.emailSender = new EmailSender();
  }

  public void sendNotification(String message) {
    emailSender.sendEmail(message);
  }
}

This code violates the key ideas of dependency inversion:

  1. High-level modules depend on concretions: NotificationService directly depends on the EmailSender class, making it inflexible.

  2. Details dictate abstractions: There's no abstraction defining how notifications are sent. The code assumes emails are the only option.

  3. Tight Coupling: Both classes are tightly coupled. Any change in EmailSender would potentially impact NotificationService.

  4. Limited Flexibility: Adding new notification methods (SMS, push notifications) becomes difficult due to the tight coupling.

  5. Testing Difficulties: Testing NotificationService in isolation becomes challenging. You'd need a real EmailSender to run the test.

Improved Version

public interface NotificationSender {
  void sendNotification(String message);
}

public class EmailSender implements NotificationSender {

  @Override
  public void sendNotification(String message) {
    // Send email implementation
  }
}

public class SmsSender implements NotificationSender {

  @Override
  public void sendNotification(String message) {
    // Send email implementation
  }
}

public class NotificationService {

  private final NotificationSender notificationSender;

  public NotificationService(NotificationSender notificationSender) {
    this.notificationSender = notificationSender;
  }

  public void sendNotification(String message) {
    notificationSender.sendNotification(message);
  }
}

// Usage:
NotificationSender emailSender = new EmailSender();
NotificationService notificationService = new NotificationService(emailSender);
notificationService.sendNotification("Your message goes here");

// Usage:
NotificationSender smsSender = new SmsSender();
NotificationService notificationService = new NotificationService(smsSender);
notificationService.sendNotification("Your message goes here");

Here, NotificationService depends on the abstract NotificationSender interface. This promotes:

  • Abstractions over Concretions: The interface defines the "what" (sending notification) without dictating the "how". The class EmailSender which implements the NotificationSender interface defines the implementation of how sending notification is done.

  • Loose Coupling: NotificationService can work with any NotificationSender implementation because now NotificationService is using an abstraction not a concrete implementation, for example SmsSender .

  • Increased Flexibility: Adding new notification types (SMS sender) is easier by implementing the NotificationSender interface, as shown in the example.

  • Improved Testability: We can mock the NotificationSender with a test double for isolated testing of NotificationService.


By embracing abstractions and loose coupling, you write code that's easier to maintain, adapt, and test. Imagine a system where changes flow smoothly instead of causing ripples of disruption. This is the power of Dependency Inversion – building software that's not just functional, but truly resilient and adaptable to the ever-evolving world of technology.


Related Articles

SOLID Principles: Single Responsibility

SOLID Principles: Open Closed Principle

SOLID Principles: Liskov Substitution Principle

SOLID Principles: Interface Segregation Principle

YouTube

Previous Videos

SOLID Principles: Single Responsibility Principle

Clean Code: Meaningful Names

Clean Code: A Demystified Intro

More from this blog

DevDemystified

14 posts

Shaped tech and product strategic direction for startups in different stages, executing strategic plans. Led, managed and mentored engineering teams, setting clear goals.