Para compartilhar essa dica, imagine que precisamos criar um registro militar para uma pessoa. Para isso, buscamos dados em três repositórios distintos:
private PersonRepository repository;
private PersonDetailsRepository detailsRepository;
private MilitaryRegistrationRepository militaryRegistrationRepository;
public void createMilitaryRegistration(final UUID personId) {
final var person = repository.findById(personId)
.orElseThrow(); // NotFoundException
// Validação 1: idade mínima
// Utiliza: person.birthdate()
final var details = detailsRepository.findById(personId)
.orElseThrow(); // NotFoundException
// Validação 2: nacionalidade brasileira
// Utiliza: details.brazilian()
// Validação 3: verificar se já existe registro
// Utiliza: militaryRegistrationRepository.exists(...)
militaryRegistrationRepository.save(
new MilitaryRegistration(
personId,
"CODE",
details.placeOfBirth() // reutilização
)
);
}
Esse código é funcional, mas mistura responsabilidades: validações e lógica de criação estão acopladas.
Em sistemas maiores, isso pode se tornar um problema.
Uma abordagem comum é extrair as validações para uma classe dedicada:
public class MilitaryRegistrationValidator {
private PersonDetailsRepository detailsRepository;
public void validate(final Person person) {
// Validação 1: idade mínima
// Utiliza: person.birthdate()
final var details = detailsRepository.findById(person.id())
.orElseThrow(); // NotFoundException
// Validação 2: nacionalidade brasileira
// Utiliza: details.brazilian()
// Validação 3: verificar se já existe registro
// Utiliza: militaryRegistrationRepository.exists(...)
}
}
E então injetar o validador no método principal:
public void createMilitaryRegistration(final UUID personId) {
final var person = repository.findById(personId)
.orElseThrow(); // NotFoundException
validator.validate(person);
final var details = detailsRepository.findById(person.id())
.orElseThrow(); // NotFoundException
militaryRegistrationRepository.save(
new MilitaryRegistration(
personId,
"CODE",
details.placeOfBirth() // reutilização
)
);
}
Mas aqui surgem alguns problemas:
O detailsRepository precisa ser injetado em dois lugares.
O método findById é chamado duas vezes para o mesmo dado.
Se a idade estiver incorreta, a consulta aos detalhes nem deveria acontecer.
A solução: Suppliers Memorizados!
Com uma simples classe utilitária, conseguimos transformar qualquer lógica de fornecimento de dados — seja uma consulta ao banco, uma chamada a API, ou até mesmo um cálculo pesado — em algo que será executado apenas uma vez, e cujo resultado poderá ser reutilizado em diferentes partes do código.
public class MemorizedSupplier<T> implements Supplier<T> {
private final Supplier<T> supplier;
private T value;
public MemorizedSupplier(final Supplier<T> supplier) {
this.supplier = supplier;
}
@Override
public T get() {
return Optional.ofNullable(value)
.orElseGet(() -> {
value = supplier.get();
return value;
});
}
}
Assim, o validador poderia receber um Supplier como argumento:
public class MilitaryRegistrationValidator {
public void validate(final Person person,
final Supplier<PersonDetails> detailsSupplier) {
// Validação 1: idade mínima
// Utiliza: person.birthdate()
final var details = detailsSupplier.get();
// Validação 2: nacionalidade brasileira
// Utiliza: details.brazilian()
// Validação 3: verificar se já existe registro
// Utiliza: militaryRegistrationRepository.exists(...)
}
}
E o método principal ficaria assim:
public void createMilitaryRegistration(final UUID personId) {
final var person = repository.findById(personId)
.orElseThrow(); // NotFoundException
final var detailsSupplier = new MemorizedSupplier<>(
() -> detailsRepository.findById(person.id()).orElseThrow()
);
validator.validate(person, detailsSupplier);
militaryRegistrationRepository.save(
new MilitaryRegistration(
personId,
"CODE",
detailsSupplier.get().placeOfBirth() // reutilização
)
);
}
Gerando os seguintes benefícios:
Eficiência: a consulta ao banco é feita apenas uma vez.
Baixo acoplamento: o repositório é usado em um único lugar.
Reutilização: o mesmo dado pode ser acessado em diferentes partes do código sem reconsultar.
Flexibilidade: o padrão pode ser aplicado em diversos contextos.
Separar responsabilidades é essencial, mas não precisa vir com custo de desempenho ou complexidade. Com Suppliers memorizados, conseguimos manter o código limpo, eficiente e alinhado com boas práticas.
OBS: a ideia apresentada neste artigo não é uma criação original minha, mas sim uma aplicação prática de um conceito funcional bem conhecido: memoization.