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()
Python

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.
  • Reusability: If a class depends on many other classes, it isn’t easy to use that class elsewhere.

Inversion of control (IoC)

Inversion of Control (IoC) is a design principle in software development in which the control of object creation and the flow of control are 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
Python

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

# Assuming MySqlConnection is defined in a separate module named database
from database import MySqlConnection  

# 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__":

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

Instead of creating instance of MySqlConnection in class UserService. It is injected by client.

db_connection = MySqlConnection('localhost', 3306, 'user', 'password')
user_service = UserService(db_connection) #injected
Python

Dependency db_connection as constructor to our UserService class

def __init__(self, db_connection):
    self.db_connection = db_connection
Python

  • 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')
Python

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)
Python

Bad Practices

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

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)
Python

Dependency Inversion Principle (DIP)

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()

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

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

cook()
Python

The cook function depends on the Bread. What if we want more items to be baked?

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

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

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

This is not good… More the food types, more the if conditions.

This breaks the SOLID principle “Open to extension, closed for modification” .Code change with each addition of food items

What next?

DIP is here to save us from such a situation.

Definition

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).

The solution: Invert the dependency

You need the cook function which is a higher-level module, not to depend on lower-level modules like Bread or Cookies. Now the right way to do this is by implementing an interface.


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

#low level
class Bread(Bakable):
    def bake(self):
        print('Smells like bread')
        
#low level
class Cookies(Bakable):
    def bake(self):
        print('Cookie smell all over the place')

# high level
def cook(bakable):
    bakable.bake()

# client code
cookies = Cookies()
bread = Bread()
cook(cookies)
cook(bread)
Python

Output

Cookie smell all over the place
Smells like bread
Python

Now the cook function depends on the abstraction. Not on the Bread, not on the Cookies but on the abstraction. By implementing the interface we are sure that every Bakable will have a bake() method that does something.

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 while cook function does not need to know. cook function will bake anything that is Bakable.

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.

Resource

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

Leave a Comment