Book cover

Buy on Leanpub (pdf and epub)

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:

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 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.


Check out the other articles on our site.