Inversion of Control and Dependency Injection with example in Spring
Inversion of Control (IOC)
These terms, IOC and DI, are mostly used in the context of frameworks. Technically, IOC can apply to any external source, but I will refer to as a framework for simplicity’s sake.
- Let’s start with IOC, IOC is a design principle where the control of certain responsibilities are handled by the framework.
- This can include object creation, dependency management or program flow.
- Let’s take a simple example of object creation for a ChessBoard, normally we would explicitly create the objects it needs and connect them up.
- But in IOC, the framework takes care of this, this is why it is referred to as ‘Inversion of Control’.
Dependency Injection (DI)
- DI is a way to implement IOC.
- DI is a technique where the object indicates the dependencies it has and the framework provides these dependencies.
- In DI, the objects don’t create the dependencies themselves.
- In frameworks, DI often uses metadata (e.g., annotations, decorations) or configurations to indicate dependencies, which are resolved and injected by the framework.
- The framework based on these annotations takes up control, handles the object creation/wiring and then it returns the flow of execution back to your application logic.
Example without IOC and DI
Let’s create a ChessBoard class which contains an array of Piece class. The main method explicitly creates the Piece objects required by the ChessBoard class and passes them to the ChessBoard constructor.
public class Main {
public static void main(String[] args) {
Piece king = new Piece("King");
Piece queen = new Piece("Queen");
ChessBoard chessBoard = new ChessBoard(new Piece[]{king, queen});
chessBoard.displayPieces(); // Piece{type='King'} Piece{type='Queen'}
chessBoard.setColor("red");
}
}
public class ChessBoard {
String color;
Piece[] pieces;
public ChessBoard(Piece[] pieces) {
this.pieces = pieces;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public void displayPieces() {
for (Piece piece : pieces) {
System.out.println(piece);
}
}
}
package org.nirmalks.ioc;
public class Piece {
String type;
public Piece(String type) {
this.type = type;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public String toString() {
return "Piece{" + "type='" + type + '\'' + '}';
}
}
Example of IOC and DI in Spring
- Let’s create the same ChessBoard in Spring. In Spring we use annotations for DI, I’m using the basic @Component and @Configuration annotation(to create the beans).
- Creating the beans in ChessConfig is the main change here and subsequently in the main program - DemoApplication, we are calling getBean method from the ApplicationContext.
- This is Spring specific, in short ApplicationContext acts as the IOC container, which provides the DI functionalities.
- In this case, it recognizes that ChessBoard requires Piece objects and it searches for it and picks up the couple of ones which had created in the ChessConfig class.
- Just to illustrate additional capabilities of the framework, In this case, Spring reuses the same Piece objects for both board1 and board2 because beans are singleton-scoped by default in Spring.
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
CommandLineRunner commandLineRunner(ApplicationContext context) {
return args -> {
ChessBoard board1 = context.getBean(ChessBoard.class);
board1.displayPieces(); // Piece{type='King'} Piece{type='Queen'}
board1.setColor("red");
ChessBoard board2 = context.getBean(ChessBoard.class);
System.out.println(board1.pieces[0] == board2.pieces[0]); //true
};
}
}
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ChessBoard {
String color;
Piece[] pieces;
@Autowired
public ChessBoard(Piece[] pieces) {
this.pieces = pieces;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public void displayPieces() {
for (Piece piece : pieces) {
System.out.println(piece);
}
}
}
package com.example.demo;
import java.util.Objects;
public class Piece {
String type;
public Piece(String type) {
this.type = type;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public String toString() {
return "Piece{" + "type='" + type + '\'' + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Piece piece = (Piece) o;
return Objects.equals(type, piece.type);
}
@Override
public int hashCode() {
return Objects.hashCode(type);
}
}
@Configuration
public class ChessConfig {
@Bean
public Piece king() {
return new Piece("King");
}
@Bean
public Piece queen() {
return new Piece("Queen");
}
}
Advantages
- Reduced coupling and increased cohesion- Class is merely declaring and not creating objects by itself.
- Increased reusability - We can reuse the same component throughtout the application.
- Increased testability and maintainability - We can simply replace the dependencies with mocks or new implementations.