Demystifyingย 

Dependency Injection

๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Jens Knipper

  • Software Engineer
  • OpenValue Dรผsseldorf
  • curious about new technologies
  • likes to share his knowledge

๐Ÿ“Œ Agenda

  • What is Dependency Injection (DI)?
    • Why should you use DI?
    • Core concepts
  • Ways to inject
    • Constructor Injection, ...
  • Wiring
    • manual vs frameworks
  • Frameworks

A

B

uses methods of

  • A creates B
  • A uses methods of B
    • B is dependency of A

creates

๐ŸŒฉ๏ธ Problem

๐ŸŒฉ๏ธ Problem

class UserService {
    UserRepository userRepo = new UserRepository(...);

    List<User> getActiveUsers() {
        return userRepo.findAll().stream()
        	.filter(User::isActive)
            .toList();
    }
}
  • violates Single Responsibility Principle
    • UserService responsible for Lifecycle of userRepo
  • tight coupling
    • cannot run UserService independently
    • difficult to test in isolation
    • changes in UserRepo might break UserService tests

โ˜€๏ธ Solution

class UserService {
    UserRepository userRepo;

	UserSerivce (UserRepository userRepo) {
    	this.userRepo = userRepo;
    }

    List<User> getActiveUsers() {
        return userRepo.findAll().stream()
        	.filter(User::isActive)
            .toList();
    }
}
  • UserRepo injected into UserService
    • lifecycle outside of UserService
    • independent testing possible

A

B

uses methods of

  • X is injector
  • A is client
  • B is dependency

X

create

create and inject B

โ˜€๏ธ Solution

๐Ÿ’‰ Dependency Injection (DI)

  • separation of concerns
    • separate construction and usage of dependencies
  • dependencies are provided (injected) from outside
  • test unit independent of its dependencies
  • many ways to do injection
    • constructor
    • field
    • ...
  • different paradigms
    • manual
    • compile time
    • runtime

A

uses methods of

  • often used with Dependency Injection
  • A does not know which implementation of B is used

X

create

create and inject B

๐Ÿ”€ย Dependency Inversion Principle

interface
B

implements

impl.
B

๐ŸŒ€ Inversion of Control (IoC)

  • control flow of a program is inverted
  • e.g. framework controls program flow
    • e.g. Spring: RequestFilter, RestController
  • loose coupling of classes
    • easy testing
  • main principle behind Dependency Injection
  • Dependency Injection is a subset of IoC
    • managing lifecycle of dependencies

๐Ÿ›ฃ๏ธ Ways to inject

  • Lot's of different names
    • Constructor injection
    • Method injection
    • Setter injection
    • Interface Injection
    • Parameter injection
    • Field injection
    • Property injection
  • often mean the same or slightly different usage

Constructor

๐Ÿ›ฃ๏ธ Ways to inject

Method

Field /Property

Setter

Interface

Parameter

๐Ÿ› ๏ธ Constructor Injection

class UserService {
    UserRepository userRepo;

	UserService (UserRepository userRepo) {
    	this.userRepo = userRepo;
    }

    List<User> getUsers() {
        return userRepo.findAll();
    }
}

Setup

UserRepository userRepository = new UserRepository();
UserService userService = new UserService(userRepository);

๐Ÿ› ๏ธ Constructor Injection

  • easy to use and understand
  • for mandatory dependencies
    • dependencies are always initialised
  • ย constructor can become bloated with dependencies

    • known as constructor over-injection

    • class might be doing more than one thing

      • refactoring!

๐Ÿšฒ Cyclic Dependencies

  • not solvable with Constructor Injection
    • refactoring!
  • use another injection type
  • framework specific solutions
UserRepository userRepository = new UserRepository(userSerivce);
UserService userService = new UserService(userRepository);

๐Ÿ“ Setter Injection

class UserService {
    UserRepository userRepo;

	void setUserRepo (UserRepository userRepo) {
    	this.userRepo = userRepo;
    }

    List<User> getUsers() {
        return userRepo.findAll();
    }
}

Setup

UserService userService = new UserService();
UserRepository userRepository = new UserRepository(userService);
userService.setUserRepo(userRepository);

๐Ÿ“ Setter Injection

  • might not be initialised when called
    • runtime errors!
  • mutability needed
    • dependencies optional
    • changeable at runtime
  • useful for lazy initialisation
    • only created when needed
      • useful when expensive to create
    • resolve cyclic dependencies

โœ๏ธ Interface Injection

  • similar to setter injection

    • dependency provided by injector method

    • defined in interface

  • dependent class (client) implements interface

  • plugin-based architecture

interface InjectUserRepo {
    public void injectUserRepo(UserRepository userRepo);
}

โš™๏ธ Parameter Injection

class UserService {
    List<User> getUsers(UserRepository userRepo) {
        return userRepo.findAll();
    }
}
  • method injection
  • stateless
  • dependency mandatory
  • changeable
  • only used when needed
  • resolve cyclic dependencies

๐Ÿ–ผ๏ธ Field Injection

class UserService {
    UserRepository userRepo;

    List<User> getUsers() {
        return userRepo.findAll();
    }
}

Setup

UserRepository userRepo = new UserRepository();
UserService userService = new UserService();
userService.userRepo = userRepo;

๐Ÿ–ผ๏ธ Field Injection (Reflection)

class UserService {
    private UserRepository userRepo;

    List<User> getUsers() {
        return userRepo.findAll();
    }
}

Setup

UserRepository userRepo = new UserRepository();
UserService userService = new UserService();
Field userRepoField = userService.getClass()
						.getDeclaredField("userRepo");
userRepoField.setAccessible(true);
userRepoField.set(userService, userRepo);

๐Ÿ–ผ๏ธ Field Injection

  • also called property injection
  • simple, avoids boilerplate code
  • difficult to test
  • dependencies optional
  • dependency changeable
  • might not be initialised when called
    • runtime errors!

๐Ÿ’ก Making a decision

  • use Constructor Injection whenever possible
    • refactor when class gets bloated
    • refactor on cyclic dependencies
  • use Setter or Parameter Injection otherwise
  • avoid Field Injection

๐Ÿ”ŒWiring

  • dependency lifecycle outside of our class
  • BUT: who manages it?
    • you: manual DI
    • someone else: a framework
class UserService {
    UserRepository userRepo;

	UserSerivce (UserRepository userRepo) {
    	this.userRepo = userRepo;
    }
}

๐Ÿช› Manual DI

public class CliTool {
	public static void main (String[] args) {
    	String endpoint = System.getenv("ENDPOINT");
		RestClient restClient = new RestClient(endpoint);
		UserService userService = new UserService(restClient);
        
        userService.doSomething();
        
        restClient.close();
    }
}
  • small applications
    • < few thousand LOC
    • CLI tools, Serverless Functions (AWS Lambda), ...
  • easy to lose overview
    • switch to a framework

๐Ÿ—๏ธ Framework

  • often annotation based
  • compile time or runtime wiring
    • different philosophies
    • works differently internally
  • IoC Container
    • manages dependencies
  • lazy or eager dependency creation
    • define creation order of dependencies
    • create dependencies when requested or at startup

๐Ÿ‘ทโ€โ™‚๏ธ Compile Time DI

  • more recent frameworks mostly
  • wiring during compilation
    • annotation processing
    • establish ordering using dependency graph
    • code generation
  • errors found during compilation: missing or cyclic dependencies
  • fast, no reflection runtime overhead
  • less flexible
    • often cannot change dependency at runtime

๐Ÿ‘ทโ€โ™‚๏ธ Compile Time DI

    • ย 
    • annotation processing
    • establish ordering using dependency graph
    • code generation

๐Ÿƒโ€โ™‚๏ธ Runtime DI

  • wiring during execution
    • classpath scanning
    • class inspection using reflection
    • establish ordering
    • object creation and wiring
  • errors might only show up at runtime
  • reflection might cause performance overhead
  • very flexible
    • easily switch dependencies using properties, profiles, ...

๐Ÿฟ Popular Frameworks

  • compile time
    • Dagger
    • Quarkus ArC
    • Avaje Inject
  • runtime
    • Spring
    • Guice
    • Jakarta CDI - e.g. MicroProfile
    • Koin

๐Ÿ’ก Making a decision

  • use the one provided by the (web) framework you use
    • well integrated
  • manual if feasible
  • compile time otherwise
    • my preference: Avaje Inject

๐ŸŒฑ Spring

  • TODO

๐Ÿช„ย Avaje Inject

  • TODO

๐ŸŒฏ Wrapping Up

  • Dependency Injection helps you with testing
  • combine with Dependency Inversion Principle for fakes and mocks
  • DI != IOC
  • lots of ways to inject
    • use constructor injection
    • refactor if not possible
    • avoid field injection
  • there is no need for a framework
    • if you have it, use it
    • prefer compile time

Resources

Thank you!

Please ask questions.

โœ‰๏ธย jens@openvalue.de

ย  ย  ย @jensknipper.de

๐ŸŒ openvalue.eu

Dependency Injection - A deep dive

By Jens Knipper

Dependency Injection - A deep dive

  • 99