Dependency Injection - Introduction

Modified on Fri, 24 Sep, 2021 at 9:07 AM

TABLE OF CONTENTS

Introduction

The relationships between the classes make it possible to extend and reuse functionality of other classes. The way we choose to build those relationships determine how decoupled and reusable our code is. 

In this article, we are going to describe the concept of Dependency Injection in Java and how it makes our life easier. So before getting to Dependency Injection, first let’s understand what dependency means.



What is a dependency ?

When a class ClassA uses any instance of another class ClassB, we can say that ClassB is a dependency of ClassA


For e.g. When the class Car uses an object of another class Wheel, class Car is dependent on class Wheel.


Milestone #1


Let us take an example of a car rental company QCar to understand this in detail. 

We have 2 classes here: Car & MRFTyre. Every car uses an instance of MRFTyre and calls the method move() to drive the car. Thus, we can say the class Car has a dependency on class MRFTyre.


Car.java

package org.crio.qcar;

public class Car {

   private CarType type;
   private String color;
   private String model;
   private MRFTyre tyre;

   public CarType getType() {
       return type;
   }

   public String getColor() {
       return color;
   }

   public String getModel() {
       return model;
   }

   public MRFTyre getTyre() {
       return tyre;
   }

   public void drive(){
       tyre.move();
   }

   @Override
   public String toString() {
       return "Car{" +
               "type=" + type +
               ", color='" + color + '\'' +
               ", model='" + model + '\'' +
               ", tyre=" + tyre +
               '}';
   }
}

MRFTyre.java

package org.crio.qcar;

public class MRFTyre {

   private double diameter;
   private String materials;

   public MRFTyre(double diameter, String materials) {
       this.diameter = diameter;
       this.materials = materials;
   }

   public double getDiameter() {
       return diameter;
   }

   public String getMaterials() {
       return materials;
   }

   public void move(){
       System.out.println("Started moving...");
   }

   @Override
   public String toString() {
       return "MRFTyre{" +
               "diameter=" + diameter +
               ", materials='" + materials + '\'' +
               '}';
   }
}

CarType.java

package org.crio.qcar;

public enum CarType {
   Hatchback,
   SUV,
   Sedan,
   SportsCar,
   Truck
}


Now, let's create a constructor for the class Car. The constructor should take the following parameters as argument: 

  1. Car type

  2. Color

  3. Model

  4. Tyre Diameter

  5. Tyre Materials


This can be done in this way.

public Car(CarType type, String color, String model, double tyreDiameter, String tyreMaterials) {
   this.type = type;
   this.color = color;
   this.model = model;
   this.tyre = new MRFTyre(tyreDiameter, tyreMaterials);
}



Activity #1

If we create two Car objects, will car1.tyre and car2.tyre have the same address or different addresses and why?


Answer: 


It will be different because each time a new MRFTyre object will be created.




Milestone #2


How does extension of functionality happen?

New Requirement has come in!


The Company QCar has been doing some experiments to improve the safety of the user and better maintenance of the car and finalised that Michelin Tyres are better for sedan cars. As a developer, you have been tasked with making required changes in the application.


Now that there are 2 types of tyres, it makes more sense to create a parent class of tyres and add suitable extensions. Thus, 2 new classes have been added now.


Tyre.java

package org.crio.qcar.tyres;

public abstract class Tyre {

   private double diameter;
   private String materials;

   public Tyre(double diameter, String materials) {
       this.diameter = diameter;
       this.materials = materials;
   }

   public double getDiameter() {
       return diameter;
   }

   public String getMaterials() {
       return materials;
   }

   public abstract void move();
}

MichelinTyre.java

package org.crio.qcar.tyres;

import org.crio.qcar.Manufacturer;

public class MichelinTyre extends Tyre{

   private Manufacturer manufacturer;

   public MichelinTyre(double diameter, String materials) {
       super(diameter, materials);
       this.manufacturer = Manufacturer.Michelin;
   }

   public void move(){
       System.out.println("Started moving with Michelin...");
   }

   @Override
   public String toString() {
       return "MichelinTyre{" +
               "diameter=" + this.getDiameter() +
               ", materials='" + this.getMaterials() + '\'' +
               '}';
   }
}

Manufacturer.java

package org.crio.qcar;

public enum Manufacturer {
   Michelin,
   MRF
}

The class MRFTyre also has been changed accordingly.

package org.crio.qcar.tyres;

import org.crio.qcar.Manufacturer;

public class MRFTyre extends Tyre{

   private Manufacturer manufacturer;

   public MRFTyre(double diameter, String materials) {
       super(diameter, materials);
       this.manufacturer = Manufacturer.MRF;
   }

   public void move(){
       System.out.println("Started moving with MRF...");
   }

   @Override
   public String toString() {
       return "MRFTyre{" +
               "diameter=" + this.getDiameter() +
               ", materials='" + this.getMaterials() + '\'' +
               '}';
   }
}


Now, the last thing we need to do is to modify the constructor of the class Car.java.


Here, we have 2 option:

  1. Add an "if" check inside the constructor to choose the proper implementation.

  2. Get Tyre as an argument in the constructor.

Both the options have their own pros and cons. Let us analyse each of them in depth.


Option 1: Choose tyre type inside the constructor:

public Car(CarType type, String color, String model, double tyreDiameter, String tyreMaterials) {
   this.type = type;
   this.color = color;
   this.model = model;
   if(type == CarType.Sedan){
       this.tyre = new MichelinTyre(tyreDiameter, tyreMaterials);
   }
   else{
       this.tyre = new MRFTyre(tyreDiameter, tyreMaterials);
   }
}


Pros:

  • No change required in the caller.


Cons:

  • This is not extensible, in future if any other tyre needs to be selected, we need to make changes in the car class, even though no other attribute gets changed.

  • We can not test the Car class independent of tyre types as we will not be able to mock different types of tyre and test the Car class for each of them.


Option 2: Get Tyre as an constructor argument:

public Car(CarType type, String color, String model, Tyre tyre) {
   this.type = type;
   this.color = color;
   this.model = model;
   this.tyre = tyre;
}


Pros:

  • This is extensible, in future, we will not need to touch the Car class if only Tyre implementation gets changed.

  • We can test the Car class independent of Tyre types by mocking appropriate Tyre implementation.


Cons:

  • The caller class logic becomes complex, as the tyre generation logic gets moved there.


If the Java class creates an instance of another class via the new operator, it cannot be used (and tested) independently from this class and this is called a hard dependency.
 




Activity #2


Run the code and print the identity hashcode of the tyre object by creating 2. objects of Sedan Car in both the ways of creating objects.


Hint:


use System.identityHashCode(<tyre_object>) to print the Identity hash code.


Takeaway:


When we have a hard dependency(in option #1), we can’t control the dependent object and it creates a different dependent object, whenever a new object needs to be created. When we don’t have the hard dependency, we can pass and reuse the dependent object and can test the object independent of the dependent object.




Milestone #3

DI to rescue: Implementation of DI with factory pattern.

We should always try to avoid hard dependency in our design to make our code future proof and easy to maintain.


Now, let us add Driver and CarFactory class to do a test drive.


CarFactory.java

package org.crio.qcar;

import org.crio.qcar.tyres.MRFTyre;
import org.crio.qcar.tyres.MichelinTyre;
import org.crio.qcar.tyres.Tyre;

public class CarFactory {

   public static Car getCar(CarType type, String color, 
                            String model, double tyreDiameter, 
                            String tyreMaterials){
       Tyre tyre = null;
       if(type == CarType.Sedan){
           tyre = new MichelinTyre(tyreDiameter, tyreMaterials);
       }
       else{
           tyre = new MRFTyre(tyreDiameter, tyreMaterials);
       }

       return new Car(type, color, model, tyre);
   }
}

Driver.java

package org.crio.qcar;

public class Driver {

   public static void main(String[] args){
       Car hatchbackCar = CarFactory.getCar(CarType.Hatchback, "Steel Grey",
               "Volkswagen Golf GTI", 26 ,"rubber");

       Car sedanCar = CarFactory.getCar(CarType.Sedan, "Starry Night",
               "Hyundai Verna", 26 ,"rubber");

       System.out.println("Driving a Hatchback...");
       hatchbackCar.drive();

       System.out.println("Driving a Sedan...");
       sedanCar.drive();
   }
}

Program Output:

Driving a Hatchback...
Started moving with MRF...
Driving a Sedan...
Started moving with Michelin...




Activity #3

If we create two Car objects, will car1.tyre and car2.tyre have the same address or different addresses and why?


Hint:


use System.identityHashCode(<tyre_object>) to print the Identity hash code.


Solution


Add the following line in CarFactory's getCar method:

System.out.println("Identity Hash code of "+type.toString()+"'s tyre in Car Factory class: "+ System.identityHashCode(tyre));

Add the following line in Car's constructor:

System.out.println("Identity Hash code of "+type.toString()+"'s tyre in Car class: "+ System.identityHashCode(tyre));

Program Output:

Identity Hash code of Hatchback's tyre in Car Factory class: 366712642
Identity Hash code of Hatchback's tyre in Car class: 366712642
Identity Hash code of Sedan's tyre in Car Factory class: 1829164700
Identity Hash code of Sedan's tyre in Car class: 1829164700
Driving a Hatchback...
Started moving with MRF...
Driving a Sedan...
Started moving with Michelin...

Takeaway:


As we can inject the dependent object in the constructor here, it’s easy for us to test the Car class independent of the Tyre class. We can also reuse the dependent object here and thus save resources.




Activity #4

A third requirement has come now! The company now has a tie-up with the world’s largest Tyre Manufacturer: Bridgestone. How would you handle this?


Answer:

 

Add the logic in the CarFactory class, no need to touch the class Car.


The Dependency Injection(DI) Principle is nothing but being able to pass (inject) the dependencies when required instead of initialising the dependencies inside of the recipient class. The goal of this technique is to remove the dependency on the lower class by separating the usage from the creation of the object. This reduces the amount of required boilerplate code and improves flexibility. 


When we need to write unit tests for our code, DI makes our job easier as now, we can mock all the dependencies and focus on testing only the logic part of the method.

The general concept behind dependency injection is called Inversion of Control (IoC). We achieve this by moving object binding from compile time to runtime.




Types of Dependency Injection

  1. Constructor Injection

    Here the dependencies are provided through the class constructor. The above example code demonstrates constructor injection. This is the most recommended way of doing dependency Injection. We can figure out the dependency just by looking at the constructor.

  2. Setter Injection

    Here, the client exposes a setter method that the injector uses to inject the dependency. In the above example, if instead of passing the Tyre object in constructor, we had exposed a public setter in the Car class and used that setter to inject the Tyre object from the caller, that would be an example of setter injection.


Change in Car.java

// Modified Constructor
public Car(CarType type, String color, String model) {
   this.type = type;
   this.color = color;
   this.model = model;
}

// Newly exposed setter
public void setTyre(Tyre tyre) {
   this.tyre = tyre;
}

Change in CarFactory.java

// Modified method
public static Car getCar(CarType type, String color, String model, double tyreDiameter, String tyreMaterials){
   ...
   Car car = new Car(type, color, model);
   car.setTyre(tyre);
   return car;
}

This is not a recommended way of doing DI, as this makes code less readable. We will not be able to figure out the dependencies from a single place unlike the Constructor injection. Also, if by mistake we forget to call the setter method[ car.setTyre(tyre);], it will throw a NullPointerException during run time.


3. Field Injection

Here, similar to "setter injection", the constructor doesn't initialise the corresponding field. Java reflection is used to inject the dependency to the required field. 

This approach has similar issues to the setter injection method. Though it is not a recommended way of doing Dependency Injection, several frameworks internally use this method for DI.



Key Takeaway

By using DI, the Car class has now become more maintainable. With the changes in the business decision on which tyre to use for a car, the Car class doesn’t need to be modified, only the Factory class will maintain the business logic and we can thus localise the change required in future.

Was this article helpful?

That’s Great!

Thank you for your feedback

Sorry! We couldn't be helpful

Thank you for your feedback

Let us know how can we improve this article!

Select at least one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article