Dependency Dynamics: Unveiling DIP, DI, and IoC

Dependency Inversion Principle (DIP), Dependency Injection (DI), and Inversion of Control (IoC) are pivotal concepts in software development, shaping the architecture and flexibility of modern applications.

What is a dependency?

A state where one class depends on another is called dependency or coupling. 

Example

class A:
    def main(self):
        b = B()
        b.some_method()
  • Class A is dependent on Class B, i.e. Class A has to create an instance of Class B
  • When Class A uses Class B
  • A can’t be used independently without B.
  • Class A is tightly coupled with Class B, changing or replacement of Class B will affect Class A
  • Testing of A is also not possible without Class B.

A good design has low dependency/loosely coupling.

Why is dependency bad? 

  • Difficult to change
  • Unit Testing: if a unit has a high dependency, it isn’t easy to test it independently.
  • Re-usability: If a class depends on many classes, then it isn’t easy to use that class elsewhere.

Inversion of control (IoC)

Inversion of Control (IoC) is a design principle in software development where the control of object creation and flow of control is inverted or “inverted” from the traditional approach.

In a traditional program, the flow of control is determined by the program logic itself, with objects being created and managed directly by the program.

Anecdote

Imagine you’re hosting a big dinner party. You want everything to run smoothly, so you hire a caterer. In traditional programming terms, you would directly instruct the caterer on every detail: what food to prepare, how to set up the tables, and when to serve each course.

Now, let’s apply Inversion of Control (IoC) to this scenario. Instead of micromanaging the caterer, you give them a general plan and let them take care of the specifics. You might say, “I want a three-course meal with vegetarian options, served buffet-style at 7 p.m.” This way, the caterer has control over how they achieve the goal within the given framework.

In software development, IoC follows a similar concept. Rather than tightly coupling components and controlling every aspect of their interaction, you define the overall structure and let a framework or container manage how components collaborate.

So, IoC flips the traditional control flow by letting higher-level structures manage the flow of control and dependencies, resulting in more flexible and modular systems.

Example: Traditional approach

class DatabaseConnection:
    def __init__(self, host, port, username, password):
        self.host = host
        self.port = port
        self.username = username
        self.password = password

    def connect(self):
        # Code to establish a database connection
        pass

class UserManager:
    def __init__(self):
        self.db_connection = DatabaseConnection('localhost', 3306, 'user', 'password')

    def get_user_data(self, user_id):
        self.db_connection.connect()
        # Code to fetch user data from the database
        pass

In this example, UserManager directly creates an instance of DatabaseConnection within its constructor, tightly coupling the two classes. This makes testing and swapping out the database connection difficult.

IoC, on the other hand, delegates the control of object creation to an outside source(external framework or container etc).

IoC aims to achieve

  • To decouple the execution of a task from implementation.
  • Decouple components and their dependencies, making the system more modular, flexible, and easier to maintain.

Quick Read

Outside Sources

  • Frameworks like Spring in Java, Service Container in Laravel, Butterknife, Dagger 2, Roboguice Google Guice etc. for Android 
  • The framework has many ways to provide IOC like via XML configuration, annotations etc.
  • Setter injection(Method Injection)
  • Property injection(Field injection)
  • Constructor 
  • Interface 
  • Annotations(@some functionality) can be used to achieve dependency injection in certain programming languages and frameworks.

What is Dependency Injection(DI)?

One of the most common implementations of IoC is through dependency injection (DI), where objects are passed their dependencies rather than creating them internally. This allows for easier testing, as dependencies can be replaced with mock objects during testing.

  • Decoupling of classes from what they depend on.
  • It is a design pattern that allows us to remove the hard-coded dependencies and make our application loosely coupled, extendable and maintainable.
  • By implementing dependency injection, we move the dependency resolution from compile-time to runtime. Dependency Inversion is a principle that emphasizes abstraction and decoupling, while Dependency Injection is a pattern and technique for implementing Dependency Inversion by externally providing dependencies to a class. They work together to improve modularity, flexibility, and testability in software systems.

Types of Dependency Injection

There are several types of Dependency Injection:

  • Constructor Injection: Dependencies are injected through the class constructor. This is one of the most common and recommended forms of DI as it ensures that dependencies are initialized when the object is created.
  • Setter Injection: Dependencies are injected through setter methods of the class. While setter injection provides flexibility in changing dependencies after object creation, it may lead to objects being in an inconsistent state if not properly managed.
  • Field Injection: Dependencies are injected directly into class fields or properties. This approach is less preferred than constructor or setter injection as it introduces tight coupling between classes and makes dependencies less explicit.
  • Interface Injection: Dependencies are injected through an interface method implemented by the class. This approach is less common and more complex than other forms of DI.

Objective is not initialize any object, inside anyother object. So that they have loosely coupling.

Examples

Constructor Injection example

# Define a class with constructor injection
class UserService:
    def __init__(self, db_connection):
        self.db_connection = db_connection

    def get_user_data(self, user_id):
        self.db_connection.connect()
        # Code to fetch user data from the database

# Usage of constructor injection
if __name__ == "__main__":
    # Assuming MySqlConnection is defined in a separate module named database
    from database import MySqlConnection  

    db_connection = MySqlConnection('localhost', 3306, 'user', 'password')
    user_service = UserService(db_connection)
    user_service.get_user_data(123)

Dependency db_connection as constructor to our UserService class

        self.db_connection = db_connection
  • db_coonection is passed to UserService as a parameter(injected via constructor).
  • UserService doesn’t have tight coupling with db_connection class.

Bad Practices

class UserService:
    def __init__(self):
        self.db_connection =  MySqlConnection('localhost', 3306, 'user', 'password')
  • We are injecting MySqlConnection by creating an instance of MySqlConnection inside UserService, which is again a tight coupling

Setter Injection example

Setter injection involves setting dependencies through setter methods. Here’s an example:

# Define a class with setter injection
class UserService:
def __init__(self):
self.db_connection = None

def set_db_connection(self, db_connection):
self.db_connection = db_connection

def get_user_data(self, user_id):
self.db_connection.connect()
# Code to fetch user data from the database

# Usage of setter injection
if __name__ == "__main__":
# Assuming MySqlConnection is defined in a separate module named database
from database import MySqlConnection

user_service = UserService()
db_connection = MySqlConnection('localhost', 3306, 'user', 'password')
user_service.set_db_connection(db_connection)
user_service.get_user_data(123)

Bad Practices

    def set_db_connection(self, db_connection):
        self.db_connection =  MySqlConnection('localhost', 3306, 'user', 'password')

Interface Injection example

from abc import ABC, abstractmethod

# Define a protocol (interface) for database connection
class DatabaseConnection(ABC):
    @abstractmethod
    def connect(self):
        pass

# Implement the protocol for MySQL database connection
class MySqlConnection(DatabaseConnection):
    def __init__(self, host, port, username, password):
        self.host = host
        self.port = port
        self.username = username
        self.password = password

    def connect(self):
        # Code to establish a MySQL database connection
        pass

# Define a class using interface injection
class UserService:
    def set_db_connection(self, db_connection: DatabaseConnection):
        self.db_connection = db_connection

    def get_user_data(self, user_id):
        self.db_connection.connect()
        # Code to fetch user data from the database


# Usage of interface injection
if __name__ == "__main__":
    user_service = UserService()
    db_connection = MySqlConnection('localhost', 3306, 'user', 'password')
    user_service.set_db_connection(db_connection)
    user_service.get_user_data(123)

Dependency Inversion Principle (DIP)

DIP suggests that high-level modules/classes should not depend on low-level modules/classes. Instead, both should depend on abstractions (interfaces or abstract classes).

Let’s say you have a program that needs you to bake bread. On a higher level, there is a point where you could call cook()

A bad way of implementing this is to make a function that cooks but also creates the bread.

def cook():
    bread = Bread()
    bread.bake()

cook()

This is not good…

As you can see, the cook function depends on the Bread.

So what happens if you want to bake cookies?

rookie mistake is to add a string parameter like this:

def cook(food: str):
    if food == "bread":
        bread = Bread()
        bread.bake()
    if food == "cookies":
        cookies = Cookies()
        cookies.bake()

cook("cookies")

This is obviously wrong. Because by adding more foods you change your code and your code becomes a mess with many if statements. And it breaks almost every principle.

The solution

So you need the cook function which is a higher-level module, not to depend on lower-level modules like Bread or Cookies

So the only thing we need is something that we can bake. And we will bake it. Now the right way to do this is by implementing an interface.

Now let’s invert the dependency!

from abc import ABC, abstractmethod
class Bakable(ABC):
    @abstractmethod
    def bake(self):
        pass

def cook(bakable:Bakable):
    bakable.bake()

And now the cook function depends on the abstraction. Not on the Bread, not on the Cookies but on the abstraction. Any Bakable can be baked now.

By implementing the interface we are sure that every Bakable will have a bake() method that does something.

But now cook function does not need to know. cook function will bake anything that is Bakable.

The dependency now goes to the client. The client is the one who wants to bake something. The client is some piece of code that is going to use cook the function. The client knows what is going to be baked.

Now by looking at the cook function, the client knows that cook function waits to receive a Bakable and only a Bakable.

So let’s create some bread.

class Bread(Bakable):
    def bake(self):
        print('Smells like bread')

Now let’s create some cookies!

class Cookies(Bakable):
    def bake(self):
        print('Cookie smell all over the place')

OK! now let’s cook them.

cookies = Cookies()
bread = Bread()
cook(cookies)
cook(bread)

Also Read

Mastering the SOLID Principles: A Developer’s Guide

https://www.youtube.com/watch?v=-3Z9L6sIAMM

IoC vs DIP

  • DIP is a specific principle within the broader concept of IoC.
  • DIP focuses on designing classes/modules with dependencies on abstractions rather than concrete implementations
  • IoC encompasses the techniques and tools (like IoC containers) used to achieve inversion of control and implement principles like DIP.

DI vs DIP

  • DI is a design pattern and a technique used to implement the Dependency Inversion Principle (DIP)
  • DI helps achieve DIP by allowing classes to depend on abstractions (interfaces) and receive concrete implementations through injection, promoting decoupling and testability.
  • Without dependency injection, there is no dependency inversion.

IoC vs DI

  • DI is a specific implementation of the IoC principle.
  • It is a design pattern where the dependencies of a class are “injected” into it from the outside, typically through constructor injection, setter injection, or interface injection.

Read More

Dependency injection is not the only pattern implementing the IoC. For example, the design patterns (but not limited) shown in the following figure are also the implementation of IoC.

Conclusion

In conclusion, the concepts of Dependency Inversion Principle (DIP), Dependency Injection (DI), and Inversion of Control (IoC) are powerful tools that empower developers to create robust and adaptable software solutions. By embracing these principles and practices, software teams can achieve greater flexibility, testability, and maintainability in their projects. As technology continues to evolve, mastering these foundational concepts remains a key aspect of modern software development practices.

1 thought on “Dependency Dynamics: Unveiling DIP, DI, and IoC”

Leave a Comment