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, SqlDataAccess
and XmlDataAccess
, implement this interface with different implementations of the get_data()
method for accessing data from SQL and XML data sources, respectively.
The 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 Checkout
class.
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.