Dependency Injection
Dependency Injection (DI) is a powerful design pattern that has become increasingly popular especially in the context of app development. It is a technique that allows the creation of objects with their dependencies supplied from the outside. This is in contrast to the traditional approach where objects are responsible for creating their own dependencies.
What is Dependency Injection?
At its core, Dependency Injection is a technique where one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it.
The intent behind DI is to achieve Separation of Concerns of construction and use of objects. This can increase readability and code reuse.
Why Use Dependency Injection?
- Loose coupling: DI helps in reducing the tight coupling between software components.
- Easier unit testing: As dependencies can be easily mocked, unit testing becomes much simpler.
- Boilerplate reduction: DI can significantly reduce the amount of boilerplate code needed to set up object dependencies.
- Flexibility: It’s easier to switch out implementations of a dependency.
When to Use Dependency Injection
You should consider using Dependency Injection when:
- You have a class with complex dependencies.
- You want to unit test your application.
- You need to maintain a codebase where dependencies change frequently.
- You’re working on a large project where managing dependencies manually would be cumbersome.
Implementing Dependency Injection
Let’s look at a simple example to illustrate how Dependency Injection works. We’ll use Python for this example.
Without Dependency Injection
class EmailService:
def send_email(self, message):
print(f"Sending email: {message}")
class NotificationService:
def __init__(self):
self.email_service = EmailService() # Dependency is created within the class
def send_notification(self, message):
self.email_service.send_email(message)
# Usage
notification_service = NotificationService()
notification_service.send_notification("Hello, World!")
In this example, NotificationService
is tightly coupled with EmailService
. If we want to change the way notifications are sent (e.g., using SMS instead of email), we’d need to modify the NotificationService
class.
With Dependency Injection
class EmailService:
def send_message(self, message):
print(f"Sending email: {message}")
class SMSService:
def send_message(self, message):
print(f"Sending SMS: {message}")
class NotificationService:
def __init__(self, messaging_service): # Dependency is injected via the constructor
self.messaging_service = messaging_service
def send_notification(self, message):
self.messaging_service.send_message(message)
# Usage
email_service = EmailService()
sms_service = SMSService()
email_notification = NotificationService(email_service)
sms_notification = NotificationService(sms_service)
email_notification.send_notification("Hello, World!")
sms_notification.send_notification("Hello, World!")
In this improved version:
-
NotificationService
doesn’t create its own messaging service. Instead, it receives it through its constructor. - We can easily switch between
EmailService
andSMSService
without changing theNotificationService
class. - It’s much easier to unit test
NotificationService
by passing in a mock messaging service.
Dependency Injection and Test Doubles
One of the most significant benefits of Dependency Injection is how it facilitates easier and more effective unit testing through the use of test doubles. Let’s explore this relationship further.
What Are Test Doubles?
Test doubles are objects that stand in for real objects in a test. They’re used when it’s impractical or undesirable to incorporate real objects into a test. There are several types of test doubles:
- Dummy: Objects that are passed around but never actually used.
- Fake: Objects that have working implementations, but usually take some shortcut which makes them not suitable for production.
- Stubs: Provide canned answers to calls made during the test.
- Spies: Stubs that also record some information based on how they were called.
- Mocks: Pre-programmed with expectations which form a specification of the calls they are expected to receive.
How Dependency Injection Facilitates the Use of Test Doubles
Dependency Injection makes it easy to use test doubles in your unit tests. Here’s how:
- Easier Substitution: Because dependencies are injected, it’s easy to substitute a real dependency with a test double in your unit tests.
- Isolation: DI allows you to isolate the unit under test by injecting test doubles for its dependencies.
- Control: You have more control over the behavior of dependencies in your tests, which allows you to test edge cases and error scenarios more easily.
Enjoy Reading This Article?
Here are some more articles you might like to read next: