Book cover
All rights reserved. Version for personal use only.
This web version is subjected to minor edits. To report errors or typos, use this form.

Home | Dark Mode | Cite

Software Engineering: A Modern Approach

Marco Tulio Valente

1 What is Dependency Injection?

1.1 Introduction

Dependency Injection is a design pattern that is not covered in the GoF book. However, the solution is commonly employed in practice and supported by many frameworks.

The concept of Dependency Injection is quite simple and essentially a literal application of its name. Let’s break it down into three steps:

1. Suppose a class PaymentService, which depends on an interface PaymentProcessor:

class PaymentService {
  private PaymentProcessor processor;
  ...
}

2. However, to follow the idea of the pattern, this class should not refer to classes that implement the PaymentProcessor interface, as in:

class PaymentService {
  private PaymentProcessor processor = new PayPalPaymentProcessor();
  ... 
}

The issue lies in the syntactical coupling of this code with a particular payment processor implementation (PayPalPaymentProcessor). In other words, the class is not prepared to accommodate changes in this dependency.

3. Therefore, a preferable solution is for PaymentService to receive this dependency via its constructor:

class PaymentService {
  private PaymentProcessor processor;

  PaymentService(PaymentProcessor processor) { // dependency injection via constructor
    this.processor = processor;
  }
  ...
}

or receive the dependency through a setter:

class PaymentService {
  private PaymentProcessor processor;
  ...
  void setPayment(PaymentProcessor processor) { // dependency injection via setter
    this.processor = processor;
  }
  ...
}

Therefore, it is now easy to understand the pattern’s name: the dependencies of a class are injected into it through parameters of either a constructor or a setter method.

Thus, the advantages of Dependency Injection are:

  • Dependency Injection simplifies the process of changing the dependencies used by a class. In our example, PaymentService is a class for processing payments, which currently uses a PayPalPaymentProcessor for this task. However, tomorrow we might decide to handle payments with a StripePaymentProcessor class instead. In such a scenario, both classes—PayPalPaymentProcessor and StripePaymentProcessor—just need to implement thePaymentProcessor interface.

  • Dependency Injection facilitates the testing of PaymentService since we can easily mock the PaymentProcessor dependency. For instance, instead of using a real payment service we can use a mock service that simulates payments. Again, this service simply needs to implement the PaymentProcessor interface. If you’re unfamiliar with the concept of mocks, please refer to the section on the subject from Chapter 8.

1.2 Dependency Injection Frameworks

One drawback of Dependency Injection is that the responsibility to create the dependencies—that is, to instantiate the objects used by a given class—is transferred to its clients, as illustrated in the following example:

class Main {
  void main() {
    PaymentProcessor processor = new PayPalPaymentProcessor();
    PaymentService service = new PaymentService(processor); 
    ...
  } 
  ...
}

However, there are frameworks that alleviate this extra burden for clients. Essentially, they assume responsibility for creating the required dependencies and injecting them into the target classes.

Generally, this works as follows:

class PaymentService {
  private PaymentProcessor processor;

  @Inject // annotation provided by the framework
  PaymentService(PaymentProcessor processor) { 
    this.processor = processor;
  }
...
}

The @Inject annotation indicates that PaymentService intends to use Dependency Injection. Consequently, the dependencies specified in its constructor should be created and injected by the dependency injection framework.

However, we should specify the classes that need to be instantiated whenever the framework calls a constructor annotated with @Inject. This can be achieved through a JSON file, as in the following example:

{ "dependencies": {
    "PaymentProcessor": {
       "class": "PayPalPaymentProcessor"
    }
    ...
}

This configuration file defines that instances of PayPalPaymentProcessor should be created and injected into all classes that depend on PaymentProcessor.

Finally, to instantiate a class that uses an @Inject annotation, we no longer use the new operator. Instead, we call a method from the framework, as showed below:

class Main {
  void main() {
    PaymentService service = DIF.getInstance(PaymentService.class);
    ...
  } 
}

In this code, getInstance is a method responsible for instantiating classes that use Dependency Injection. Thus, our hypothetical dependency injection framework (DIF) comprehends that:

  • The class PaymentService relies on dependency injection, as its constructor is annotated with @Inject.

  • Then, before instantiating an object of this class, the framework should instantiate the dependencies required by the class constructor. In our case, these dependencies must implement the PaymentProcessor interface.

  • However, which class that implements PaymentProcessor should be instantiated? For this, the framework consults the configuration file and discovers that PaymentProcessor is mapped to PayPalPaymentProcessor, as we showed earlier.

  • Consequently, the framework instantiates an object of type PayPalPaymentProcessor and an object of type PaymentService. While instantiating this latter object, it passes the PayPalPaymentProcessor object as a parameter to its constructor.

The explanation provided above is based on Guice, a Dependency Injection Framework for Java developed by Google. However, there are other frameworks for this purpose, both for Java and for other programming languages.

Exercises

1. As described in Chapter 6, design patterns are categorized into creational, structural, and behavioral patterns. Within which of these categories would you place Dependency Injection? Please provide a brief justification for your answer.

2. Dependency Injection is often compared to the Factory design pattern. What is the disadvantage of configuring dependencies using factories? To answer, compare the following codes:

class A {
  IB b;
  A(IB b) { 
    this.b = b; // dependency injection 
  }
}
class A {
  IB b;
  A() { 
    this.b = IB_Factory.getInstance(); // factory
  }
}

3. What is the relationship between Dependency Injection (design pattern) and Dependency Inversion (design principle)? To learn more about Dependency Inversion, see Chapter 5.

4. Why is it often mentioned that Dependency Injection can compromise the Information Hiding property? To learn more about this property, see also Chapter 5.