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.
Table of Contents
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.
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?
A 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.
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”