Monday, April 1, 2019

What is dependency injection and what are the advantages?

Dependency injection is one form of Inversion of Control (IoC).  Instead of an object creating or being statically linked to implementations of a dependency, dependency injection allows dependencies to be provided to the object.

A simple example of a class that does not allow dependencies to be injected in any way is shown below.

public class UserAuthenticationService {
    private final UserRepository userRepository;

    public UserAuthenticationService() {
        this.userRepository = new DatabaseUserRepository();
    }

    public UserProfile authenticate(String username, String password) {
        return this.userRepository.findUser(username, password);
    }
}

The constructor creates its own instance of a DatabaseUserRepository. If a consumer of the UserAuthenticationService would like to use it with a different UserRepository, he/she does not have an opportunity to do so. Even if he/she did want to use the DatabaseUserRepository but wanted to pass additional arguments during construction (perhaps a non-default username/password), there is still no opportunity to make those changes without changing the implementation of the UserAuthenticationService.

There are multiple ways this problem can be rectified. The simplest way is to update the constructor to accept a UserRepository.

public class UserAuthenticationService {
    private final UserRepository userRepository;

    public UserAuthenticationService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public UserProfile authenticate(String username, String password) {
        return this.userRepository.findUser(username, password);
    }
}

Spring, like other frameworks, provides additional ways to inject dependencies.  Using the Spring provided @Autowired annotation or the JSR-330 provided @Inject annotation, Spring can inject dependencies into constructors, setter methods, and fields. An example of each is shown below with some thoughts on advantages and disadvantages of each.  Inject has been used instead of @Autowired because it gives a little more flexibility to switch between frameworks instead of being tied directly to Spring.

Constructor Injection
public class UserAuthenticationService {
    private final UserRepository userRepository;
    
    @Inject
    public UserAuthenticationService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public UserProfile authenticate(String username, String password) {
        return this.userRepository.findUser(username, password);
    }
}

With constructor injection, when Spring creates the UserAuthenticationService bean, it will inject an instance of a UserRepository bean from the application context.  Constructor injection has the advantage that the class can be used outside of a dependency injection framework without any changes, the injected dependency can be assigned to a final field preventing any code from accidentally or maliciously changing the value, and any setup that needs to occur after dependency injection but before the class can be used can take place in the constructor.  The drawback to this approach occurs if the service is created through Java config then the method that creates the UserAuthenticationService bean must have dependencies injected into it to pass along to the constructor explicitly.  As the number of the dependencies of the service increases, the number of arguments to the constructor will also increase as well as the number of arguments in the bean constructor method.

Field Injection
public class UserAuthenticationService {
    @Inject
    private UserRepository userRepository;
    
    public UserAuthenticationService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public UserProfile authenticate(String username, String password) {
        return this.userRepository.findUser(username, password);
    }
}

Field injection injects dependencies directly into a field of a bean after construction.  This method of dependency injection can be less flexible than constructor dependency injection because any initialization that must be done after dependencies are injected must be performed in a separate method.  A method annotated with @PostConstruct will be called automatically by dependency injection frameworks, but users not using dependency injection frameworks will have to know to call this method manually.  In addition, injected fields cannot be marked as final due to injection occurring after class construction.

Setter Injection
public class UserAuthenticationService {
    private UserRepository userRepository;

    @Inject
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public UserProfile authenticate(String username, String password) {
        return this.userRepository.findUser(username, password);
    }
}

Setter injection, as the name suggests, uses setters to receive dependency injections.  Like constructor injection, setter injection allows the class to be used outside of a dependency injection framework without any changes.  Unlike constructor injection, member variables cannot be marked as final due to the setter allowing mutation.  Setter dependency injection is very similar to field dependency injection except it gives a chance to perform additional logic after individual dependencies are injected (inside the setter method).  If any work should be performed after all dependencies are injected then a method annotated with @PostConstruct like that used in field injection will be needed.

No comments:

Post a Comment