Injeções de dependências: setters, constructor e field injection

Reflito há algum tempo sobre os códigos que li e os quais possuem diferentes tipos de injeções de dependências:

  1. configurações de XML para injetar através de métodos setters
  2. atributos declarados com @Autowired
  3. os atributos como final que usam construtores para serem inicializados

A discussão parece irrelevante e se tratar apenas de uma questão de "estilo" mas podem trazer um caos para a manutenção do sistema.

Field e setter injection

Há uma motivação histórica que explica o intenso uso dos setters, especialmente em sistemas legados. As primeiras releases de Spring foram construidas com injeções em setters e as configurações para usar o framework incentivava o uso de setters mas isso era meados de 2003....

Considere a seguinte classe:

public class MyService {
 
     private MyExample myExample; 
 
     @Autowired
     public void setMyExample(MyExample myExample) {
        this.myExample = myExample;
     }
     
     public String toString() {
        return myExample.toString();
    }
 }

Enquanto com field injection temos poucas linhas de código e é extremamente fácil:

public class MyService {
    @Autowired
    private MyExample myExample;
    
    public String toString() {
        return myExample.toString();
    }
 }

Note que em ambos os casos é possível criar uma instância da classe:

MyService myService = new MyService();
System.out.println(myService.toString()); 

O grande problema é que isso resultará nullPointerException e esse cenário é perfeitamente possível de ocorrer, por exemplo, em testes unitários/integração em que é necessário injetar as dependências ou aplicações que não são 100% Spring e as quais possuem classes fora do "contexto" Spring.

Há quem defenda que dependendo do contexto pode fazer sentido você ter uma instância da classe com determinados atributos nulos, algo que me parece mais exceção e code smell.

Reescrevendo com a injeção no construtor:

 public class MyService {
    
    private final MyExample myExample;
    
    @Autowired
    public MyService(MyExample myExample) {
        this.myExample = myExample;
    }
 }

Essa abordagem é mais vantajosa principalmente por trazer robustez ao permitir declarar como final e trás mais segurança ao código não permitindo criar instâncias de MyService com MyExample nulo. Para quem gosta de criar testes com builders e mocks, os construtores facilitam e muito na hora de escrever os testes não permitindo que você esqueça de testar nada.

Um problema que isso pode trazer é o excesso de argumentos nos construtores e é um bom indicador para se questionar: "essa classe realmente está fazendo só o que é necessário?"

Há algumas técnicas de refatoração nesse caso como separar em novas classes ou construir construtores específicos:

Constructors with parameters give you a clear statement of what it means to create a valid object in an obvious place. If there's more than one way to do it, create multiple constructors that show the different combinations.

O único ponto negativo é ser bastante verboso, nesse caso o Projeto Lombok pode ajudar:

 @RequiredArgsConstructor(onConstructor = @__(@Autowired))
 public class MyService {
    
    private final @lombok.NonNull MyExample myExample;
   
 }

Mas cuidado com a Annotatiomania!

Curiosidades

  • O IntellIJ incluiu há um tempo o warning de que field injection pode causar nullPointerException
  • Hoje é praticamente consenso entre colaboradores do Projeto Spring em optar por injeção através de construtores

Leituras complementares

Why Field Injection is Evil
http://olivergierke.de/2013/11/why-field-injection-is-evil/
Inversion of Control Containers and the Dependency Injection pattern
https://martinfowler.com/articles/injection.html#ConcludingThoughts
Setter injection versus constructor injection and the use of @Required
https://spring.io/blog/2007/07/11/setter-injection-versus-constructor-injection-and-the-use-of-required/

Show Comments