Programação defensiva
No contexto de uma linguagem orientada a objetos, programação defensiva consiste em o código de uma classe não permitir que seus objetos sejam criados ou colocados em um estado inválido. As regras a seguir contribuem para que isso ocorra.
As regras valem para o código principal, e não para o de teste. Um teste precisa, justamente, provocar situações inválidas — passar null para verificar que um construtor o rejeita, por exemplo —, e os frameworks de teste exigem atributos mutáveis: as step definitions do Cucumber guardam, entre um passo e outro, o resultado da operação e a exceção capturada, e os testes do JUnit recebem as dependências por @Autowired e as preparam em @BeforeEach.
As entidades JPA são outra exceção às regras de Optional, de atributos final e de validação de argumentos. O JPA exige atributos não final e um construtor sem argumentos, e um atributo opcional da entidade é null, anotado com @Nullable. Suas restrições são verificadas pelo Bean Validation antes de cada gravação, e não pelo construtor; veja Validação das entidades JPA. As regras do domínio continuam garantidas pelo agregado e por seus objetos de valor, a partir dos quais a entidade JPA é criada.
Evite null
Todo pacote não vazio deve ser anotado com @NullMarked.
Em projetos que herdam de d5-pai, essa regra é verificada na compilação do código principal — o de teste não é verificado: o nullability-maven-plugin configura o Error Prone e o NullAway, que fazem o build falhar quando uma classe está em um pacote não anotado com @NullMarked ou quando o código pode atribuir ou devolver null onde ele não é admitido.
Atributos, parâmetros ou tipos de retorno não obrigatórios devem ser do tipo Optional.
A exceção são os contratos de bibliotecas e frameworks que não admitem Optional: neles, um valor ausente é null, anotado com @Nullable. É o caso do parâmetro de equals, que sobrescreve Object.equals, como em IObjetoValor, e dos parâmetros das consultas Spring Data JPA declaradas com @Query, em que um filtro ausente é null — por isso, paraLike devolve null na ausência de filtro.
Atributos final
Atributos devem ser declarados como final.
Classes final
As classes concretas do domínio e da aplicação — agregados, objetos de valor, serviços, autorizações, projeções e consultas — devem ser declaradas como final. Uma subclasse poderia sobrescrever seus métodos e, assim, contornar as validações que mantêm seus objetos em um estado válido.
A regra não vale para as classes que o Spring ou o JPA estendem em tempo de execução, criando subclasses — os proxies — que interceptam as chamadas a seus métodos:
- as entidades JPA, que a especificação do Jakarta Persistence não admite como
final; - as classes anotadas com
@Configurationou@SpringBootApplication, cujos métodos@Beano Spring intercepta; - os controladores cujos métodos são mapeados para
POST, que o framework envolve em uma transação; veja Gerenciamento de transações.
Validação de argumentos
Construtores e métodos devem validar seus argumentos com Objects.requireNonNull e os métodos da classe Validate da biblioteca Commons Lang.
Validação de estado
Métodos devem validar se o estado atual do objeto e da aplicação como um todo permitem sua execução. Em caso negativo, devem lançar IllegalStateException.
Os argumentos são validados antes do estado: um argumento inválido torna a execução impossível em qualquer estado, e validá-lo primeiro faz a exceção lançada apontar o motivo real da rejeição. A exceção é a existência do objeto sobre o qual a operação age: se ele não existe, a operação fica sem objeto, e essa ausência é o motivo real da rejeição, ainda que os argumentos também sejam inválidos. Por isso, na alteração de um agregado, AgregadoServicoAbstrato verifica a existência antes de validar o agregado recebido.