Dependency Injection in Python which is also known as DI, is a design pattern used in software engineering to manage the dependencies between different components of an application. The basic idea behind DI is to decouple the creation of an object from its dependencies and provide them externally. In other words, instead of creating objects inside other objects, the dependencies are injected into the objects from outside.
The main benefits of using DI are:
- Testability: By injecting dependencies into an object, it becomes easier to write automated tests for the object. This is because the dependencies can be replaced with mock objects or test doubles that mimic the behaviour of the real dependencies.
- Flexibility: By injecting dependencies into an object, it becomes easier to change the behaviour of the object at runtime without modifying its code. This is because the dependencies can be swapped out with different implementations of the same interface.
There are three types of dependency injection:
- Constructor Injection: Dependencies are injected via a class constructor.
- Setter Injection: Dependencies are injected via a set method or property.
- Interface Injection: Dependencies are injected via an interface that the object implements.
Examples of 3 Dependency Injection:
Here’s an example of constructor injection in Python:
Suppose you have a class
Car that depends on a class
Engine to function. Instead of creating an instance of
Engine inside the
Car class, you can inject the dependency via the constructor:
class Engine: def start(self): print("Engine started.") class Car: def __init__(self, engine): self.engine = engine def start(self): self.engine.start() print("Car started.") engine = Engine() car = Car(engine) car.start() # Output: "Engine started. Car started."
In the above example, the
Car class depends on the
Engine class to function. The
Engine class is injected into the
Car class via the constructor. When the
start() method of the
Car class is called, it calls the
start() method of the injected
Engine object first and then prints “Car started.” This way, the
Car class is decoupled from the implementation details of the
Engine class and can be easily tested and modified.
Example of setter injection in Python:
class User: def __init__(self): self.db_connection = None def set_db_connection(self, db_connection): self.db_connection = db_connection def get_user(self, user_id): # use the db_connection to retrieve user information
In this example, the
User class has a setter method called
set_db_connection that takes a database connection object as its argument. The setter method sets the
db_connection instance variable of the
User class to the provided connection object. The
get_user method of the
User class uses the
db_connection instance variable to retrieve user information from the database.
To use the
User class with setter injection, you first create an instance of the class and then inject the database connection object using the
set_db_connection method, like this:
from some_db_module import create_db_connection user = User() db_connection = create_db_connection() user.set_db_connection(db_connection) user.get_user(123)
In this example, the
create_db_connection function returns a database connection object that is then injected into the
User instance using the
set_db_connection method. This way, the
User class is decoupled from the implementation details of the database connection and can be easily tested and modified.
Example of Interface Injection in python:
Interface injection is a type of dependency injection where an object depends on an interface rather than a concrete implementation. This allows the object to be more flexible and configurable at runtime, as different implementations of the interface can be injected.
Here is an example of interface injection in Python:
class IDataAccess: def get_data(self, query): pass class SqlDataAccess(IDataAccess): def get_data(self, query): # implementation for SQL data access pass class XmlDataAccess(IDataAccess): def get_data(self, query): # implementation for XML data access pass class DataAccessService: def __init__(self, data_access): self.data_access = data_access def get_data(self, query): return self.data_access.get_data(query)
In this example, there is an interface called
IDataAccess that defines a method called
get_data(). Two classes,
XmlDataAccess, implement this interface with different implementations of the
get_data() method for accessing data from SQL and XML data sources, respectively.
DataAccessService class depends on the implementation of the IDataAccess interface, which is injected via the constructor. This allows the DataAccessService class to be more flexible and configurable, as different implementations of the IDataAccess interface can be injected at runtime.
Here’s an example of how you could use this code:
sql_data_access = SqlDataAccess() data_access_service = DataAccessService(sql_data_access) result = data_access_service.get_data("SELECT * FROM customers") xml_data_access = XmlDataAccess() data_access_service = DataAccessService(xml_data_access) result = data_access_service.get_data("SELECT * FROM customers")
In this example, two instances of the
DataAccessService class are created with different implementations of the
IDataAccess interface injected. The first instance uses the
SqlDataAccess implementation to access SQL data, while the second instance uses the
XmlDataAccess implementation to access XML data.
Real Time Example Dependency Injection:
So here is a real-time example of Dependency Injection in action:
Suppose you are building an e-commerce platform that allows users to buy and sell products online. The platform has a
Checkout class that handles the checkout process. The
Checkout class depends on a payment gateway to process the payment.
Without Dependency Injection, the
Checkout class would create an instance of the payment gateway directly. Here’s an example:
class PaymentGateway: def process_payment(self, amount): # process the payment class Checkout: def __init__(self, amount): self.payment_gateway = PaymentGateway() self.amount = amount def process_checkout(self): self.payment_gateway.process_payment(self.amount)
In this example, the
Checkout class creates an instance of the
PaymentGateway class inside its constructor. This creates a tight coupling between the
Checkout class and the
PaymentGateway class. If you want to use a different payment gateway or mock the
PaymentGateway class for testing purposes, you need to modify the code of the
With Dependency Injection, the
Checkout class can receive an instance of the payment gateway via its constructor. Here’s an example:
class Checkout: def __init__(self, amount, payment_gateway): self.payment_gateway = payment_gateway self.amount = amount def process_checkout(self): self.payment_gateway.process_payment(self.amount)
In this example, the
Checkout class receives an instance of the
PaymentGateway class as a parameter in its constructor. This way, the
Checkout class is decoupled from the
PaymentGateway class, and we can easily switch to a different payment gateway or use a mock object for testing purposes.
Here’s an example of how we could use the
Checkout class with Dependency Injection:
# Create an instance of the PaymentGateway class payment_gateway = PaymentGateway() # Create an instance of the Checkout class with the PaymentGateway object injected checkout = Checkout(100, payment_gateway) # Process the checkout checkout.process_checkout()
In this example, we create an instance of the
PaymentGateway class and inject it into an instance of the
Checkout class. We can now call the
process_checkout() method on the
Checkout object and it will use the injected
PaymentGateway object to process the payment.