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()
PythonClass 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
PythonIn 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
# 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)
PythonInstead 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
PythonDependency 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')
PythonWe 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)
PythonBad Practices
def set_db_connection(self, db_connection):
self.db_connection = MySqlConnection('localhost', 3306, 'user', 'password')
PythonInterface 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)
PythonDependency 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()
PythonThe 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")
PythonThis 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)
PythonOutput
Cookie smell all over the place
Smells like bread
PythonNow 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.
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”