Zum Inhalt springen

Dica Java: Memorized Supplier #009

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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert