Skip to main content

Solid Principles

Dependency inversion principle

D — DIP— Dependency inversion principle

Definitions

Depend in the direction of abstraction. High level modules should not depend upon low level details.Depend upon abstractions, not concretions.high-level modules (which contain the main business logic) should not depend on low-level modules (details of implementation); instead, both should depend on abstractionsAbstractions should not rely on details; instead, details should depend on abstractions

Normal flow of control

Without dependency inversion

With dependency inversion

With dependency inversion

DIP promotes the use of abstractions and interfaces to decouple high-level and low-level components, enhancing flexibility and reducing dependencies in software systems. In OO, this means that clients should depend on interfaces rather than concrete classes as much as possible. This ensures that code is relying on the smallest possible surface area — in fact, it doesn’t depend on code at all, just a contract defining how that code should behave. As with other principles, this reduces the risk of a breakage in one place causing breakages elsewhere accidentally.
DIP complements other SOLID principles like Single Responsibility Principle (SRP), Open-Closed Principle (OCP), and Liskov Substitution Principle (LSP).

Let’s go through few code examples

/***
Violating DIP:
  High-level application code directly calls framework class
*/
public class MyApplication {
    public void sendMessage(String message) {
        MessagingFramework.send(message); // Tight coupling to framework
    }
}
Applying DIP:
// Introduce an abstraction for messaging:
interface MessageService {
    void send(String message);
}

// Framework implements the abstraction:
class MessagingFramework1 implements MessageService {
    // ... implementation for sending messages
}

// Framework implements the abstraction:
class MessagingFramework2 implements MessageService {
    // ... implementation for sending messages
}

// Application depends on the abstraction:
public class MyApplication {
    private MessageService messageService; // Dependency injected

    public MyApplication(MessageService messageService) {
        this.messageService = messageService;
    }

    public void sendMessage(String message) {
        messageService.send(message); // Decoupled from framework
    }
}

Example-2

// High-level service directly uses a database class:
public class CustomerService {
    public void saveCustomer(Customer customer) {
        Database db = new Database();
        db.save(customer); // Tight coupling to database
    }
}// Introduce an abstraction for data access:
interface CustomerRepository {
    void save(Customer customer);
}

// Database implementation:
class DatabaseCustomerRepository implements CustomerRepository {
    // ... implementation for saving to the database
}

// Service depends on the abstraction:
public class CustomerService {
    private CustomerRepository customerRepository; // Dependency injected

    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public void saveCustomer(Customer customer) {
        customerRepository.save(customer); // Decoupled from database
    }
}

Example-3

// High-level component directly calls a logging library:
public class Processor {
    public void processData() {
        // ... processing logic
        Logger.log("Processing complete"); // Tight coupling to logging library
    }
}// Introduce an abstraction for logging:
interface Logger {
    void log(String message);
}

// Logging library implementation:
class FileLogger implements Logger {
    // ... implementation for logging to a file
}

// Component depends on the abstraction:
public class Processor {
    private Logger logger; // Dependency injected

    public Processor(Logger logger) {
        this.logger = logger;
    }

    public void processData() {
        // ... processing logic
        logger.log("Processing complete"); // Decoupled from logging library
    }
}

Confusion between Dependency inversion principle and Dependency injection.

dependency injection (DI) and dependency inversion principle (DIP) are related concepts, but they’re not the same thing. Here’s how they differ:

Dependency Inversion Principle (DIP):

  • Defines a high-level design principle: It states that high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions shouldn’t depend on details; details should depend on abstractions.
  • Focuses on design and decoupling: It aims to create loosely coupled modules that are independent of specific implementations.
  • Doesn’t specify how dependencies are managed: It only dictates the direction of dependencies.

Dependency Injection (DI):

  • Defines a specific technique: It’s a technique for implementing DIP where dependencies are provided to a module at runtime instead of being hardcoded or created by the module itself.
  • Focuses on implementing DIP: It’s a practical way to achieve the decoupling and flexibility promoted by DIP.
  • Provides a way to manage dependencies: It defines mechanisms like dependency injection containers or constructor injection to provide dependencies at runtime.

In simpler terms:

  • DIP is the “what” — what kind of relationship your modules should have: less concrete, more high-level.
  • DI is the “how” — how you achieve that relationship: more specific, focuses on implementation.

Inversion of control (IoC) is a broader concept closely related to the dependency inversion principle (DIP). Here’s how they connect:

Inversion of Control (IoC):

  • Describes a general principle: It states that the control flow of a program should not be dictated by its own objects but rather by an external entity.
  • Focuses on who controls the flow: It’s about shifting the responsibility of managing object interactions from the objects themselves to an external mechanism.
  • Encompasses various techniques: DI is a specific implementation of IoC, but other techniques like service locators and event-driven programming also fall under the IoC umbrella.

Relationship Between IoC, DIP, and DI:

  • DIP provides the design guidelines: It specifies that high-level modules should depend on abstractions, not concretions, promoting loose coupling.
  • IoC offers a broader concept: It suggests inverting the control flow to achieve this decoupling, allowing external mechanisms to manage dependencies.
  • DI is a specific IoC technique: It implements DIP by providing dependencies to modules at runtime, enabling flexible and testable code.

Implementation guidelines for DIP

1. Identify Dependencies: Analyze your code to determine which modules depend on other modules. Look for concrete implementations being directly invoked instead of abstractions.

2. Define Abstractions: Create interfaces or abstract classes that define the contracts for dependencies. These abstractions should capture the essential functionality needed by the dependent modules without specifying concrete implementations.

3. Depend on Abstractions: Ensure that high-level modules depend on the abstractions you defined (interfaces or abstract classes) instead of specific concrete implementations. This decoupling allows for flexibility and easier switching between implementations.

4. Implement Abstractions: Develop concrete implementations of the defined abstractions. These implementations can offer different functionalities while upholding the same contract defined by the abstraction.

5. Use Dependency Injection: Inject dependencies into your high-level modules at runtime instead of having them create or acquire dependencies themselves. This enables easier testing and allows for dynamic configuration of components.

Tip:

Favour composition over inheritance: Compose multiple smaller abstractions to build more complex functionalities rather than relying on large, hierarchical inheritance structures. This enhances modularity and adherence to DIP.

Now that we have read all the SOLID design principles, Let’s find answers to questions we asked in part-1 of this article series.

Are these principles applicable to every problem we solve as software engineers?
These principles are not magic spells, but helpful guidelines for most software problems. Like tools in a toolbox, useful in many situations, but not always the perfect fit.

Is there a way to know if I’m violating these principles? Can we use a tool, like a linter, to alert us when we break these rules?
Some tools can sniff out potential problems, but you’re the detective! Watch for clues like long methods, tight connections between classes, and overly broad responsibilities.

Are these principles only meant for object-oriented programming languages?
Nope! They work in many languages, even those without objects. It’s all about building clean, flexible code, no matter the language.

How can I differentiate between the different principles in SOLID? They seem quite similar to me.
They share some family traits, but each shines in a different way. Single Responsibility keeps each part focused, Open-Closed lets you add features without breaking things, Liskov Substitution makes sure replacements fit seamlessly, Interface Segregation prefers smaller, focused connections, and Dependency Inversion flips the script, making high-level parts rely on general ideas, not specific tools.

To fix a code which violates a SOLID principles, Its solution is almost same that we used to solve other SOLID principle. Why is that, Why the code structure provided in examples of each principle looks the same.
All SOLID principles aim for similar qualities in code: — Clear responsibilities: Each part has a focused job. — Loose coupling: Parts aren’t tightly tangled together. — Flexibility: Easy to change or extend without breaking things. — Maintainability: Simple to understand and update.

Can we apply these principles to new code in a project, or are they only useful for existing projects that don’t currently follow these principles?
No way! Existing code can get a makeover too! Refactoring with SOLID principles can breathe new life into old code, making it easier to maintain and update.

How much emphasis should I put on each principle? Is one more important than others?
All principles matter, but some might be more important depending on the situation. Open-Closed might be king for a library, while Single Responsibility might be the main knight for a simple script. Use your judgment to pick the most relevant champion for each battle.

Are there trade-offs to using SOLID? Do they take longer to implement?
SOLID might take a bit longer at first, but it’s an investment that pays off. You’ll save time later with easier maintenance, fewer bugs, and smoother code updates.

How do I prioritize applying SOLID when deadlines are tight?
Be a smart hero! Focus on the principles that offer the biggest gains right now, like reducing connections or improving clarity. You can always tackle the other dragons later, one at a time.

What resources can help me learn SOLID better? Books, tutorials, online courses?
Knowledge is power! Check out books like “Clean Code” or online courses. Learning SOLID is a journey, not a destination, so keep exploring and practicing!

How can I use SOLID to write cleaner and more maintainable code?
With SOLID as your guide, your code can become a shining example of clarity and maintainability. Remember, it’s not a rigid rulebook, but a flexible set of tools. Use them thoughtfully to craft elegant, sustainable code that shines!

In conclusion, the SOLID principles serve as a guiding set of principles in object-oriented programming, promoting the development of robust, maintainable, and scalable software. By emphasizing concepts such as single responsibility, open/closed design, substitution, interface segregation, and dependency inversion, SOLID provides a framework for creating flexible and adaptable code. Adhering to these principles leads to codebases that are less prone to bugs, easier to extend, and more accommodating to changes. Ultimately, embracing SOLID principles contributes to a foundation of best practices that fosters a higher standard of software design, making codebases more resilient and facilitating long-term sustainability.