Validação de regras: evitando blocos de ifs

Leandro Lima - Jul 31 '20 - - Dev Community

O Problema 🤯

Lembro de trabalhar em um projeto em que eu tinha que validar vários dados que vinham de uma entrada de usuário e as validações eram regras de negócio complexas. Naquela época eu escrevi um método que possuía uma quantidade grande de if's.

Fiquei bastante contrariado com aquele código. Ele não era escalável, ele desobedecia à ideia de single responsability. Me sentia envergonhado com ele, mas não fui capaz de achar uma solução com o prazo apertado que tinha e ele foi ficando na minha cabeça sem solução.

Imagine que temos uma esteira onde um pacote de pão de queijo, como bom mineiro, adoro o ouro de Minas, é verificado quanto à sua qualidade e quantidade:

  1. Quantidade de pães de queijo correta
  2. Peso da embalagem
  3. Volume de cada pão de queijo
  4. Peso de cada pão de queijo

Nossa principal entidade é pão de queijo.

import lombok.Data;

public @Data class PaoDeQueijo {
    private double circumferencia;
    private double peso;

    public double getVolume() {
        return 4./3 * Math.PI * Math.pow(circumferencia/(2 * Math.PI), 3);
    }
}
Enter fullscreen mode Exit fullscreen mode

Um pacote de pães de queijo nada mais é do que uma lista.

public final class PacotePaoQueijo extends ArrayList<PaoDeQueijo> {
}
Enter fullscreen mode Exit fullscreen mode

Solução com vergonha 😞

public class VerificadorPacotePaoQueijoOld {

    public static final int QUANTIDADE_PAO_DE_QUEIJO_PACOTE = 10;
    public static final float PESO_PACOTE = 500f;
    public static final double MIN_VOLUME_PAO_DE_QUEIJO = 1.;
    public static final double MAX_PAO_DE_QUEIJO_VOLUME = 1.1;
    public static final double MIN_PESO_PAO_QUEIJO = 50;
    public static final double MAX_PESO_PAO_QUEIJO = 51;

    public boolean isPaoDeQueijoValido(PacotePaoQueijo pacote) throws PacoteDefeituosoException {
        final int quantidade = pacote.size();
        if(quantidade != QUANTIDADE_PAO_DE_QUEIJO_PACOTE)
            throw new PacoteDefeituosoException(String.format("Quantidade de pães de queijo errada. Esperava %d e " +
                            "encontrou %d.", QUANTIDADE_PAO_DE_QUEIJO_PACOTE, quantidade));

        final double pesoPacote = pacote.stream().map(PaoDeQueijo::getPeso).reduce(0., Double::sum);
        if (pesoPacote != PESO_PACOTE)
            throw new PacoteDefeituosoException(String.format("O peso do pacote de pães de queijo não está correto. " +
                            "Esperava %f g e encontrou %f g.",
                    PESO_PACOTE, pesoPacote));

        for (PaoDeQueijo paoDeQueijo : pacote) {
            try {
                validarPaoDeQueijo(paoDeQueijo);
            } catch (PaoDeQueijoDefeituosoException e) {
                throw new PacoteDefeituosoException("Esse pacote possui pães de queijo defeituosos.", e);
            }
        }

        return true;
    }

    public void validarPaoDeQueijo(final PaoDeQueijo paoDeQueijo) throws PaoDeQueijoDefeituosoException {
        final double peso = paoDeQueijo.getPeso();
        if(peso < MIN_PESO_PAO_QUEIJO || peso > MAX_PESO_PAO_QUEIJO)
            throw new PaoDeQueijoDefeituosoException(String.format("O pão de queijo encontra-se fora da faixa de peso permitida." +
                    "Esperava entre %f g e %f g e encontrou %f g.", MIN_PESO_PAO_QUEIJO, MAX_PESO_PAO_QUEIJO, peso));


        final double volume = paoDeQueijo.getVolume();
        if(volume < MIN_VOLUME_PAO_DE_QUEIJO || volume > MAX_PAO_DE_QUEIJO_VOLUME)
            throw new PaoDeQueijoDefeituosoException(String.format("Pão de queijo com volume errado. Esperava valores entre " +
                    "%f e %f e encontrou %f", MIN_VOLUME_PAO_DE_QUEIJO, MAX_PAO_DE_QUEIJO_VOLUME, volume));
    }
}
Enter fullscreen mode Exit fullscreen mode

Nessa solução até separamos os métodos para verificar pacote e pão de queijo, usamos exceções adequadas, mas perceba, tratamos dois tipos de dados diferentes e temos inúmeros if's. O que aconteceria se você precisasse adicionar mais validações? Poderíamos validar temperatura, quantidade de queijo, sal, etc. Essa classe iria crescer e ficar monstruosa. Mesmo que separássemos a parte de regras para pão de queijo em uma nova classe ainda sim, seria ruim.

Imagine quanto é chato testar esse código, um método que valida tanta coisa geraria testes com várias validações. Fica claro que o single responsability principle não é respeitado.

A solução do livramento 🙌

@Log4j
public class EsteiraAvaliacaoPaesDeQueijo {
    private final List<Regra<PaoDeQueijo, PaoDeQueijoDefeituosoException>> regrasPaoDeQueijo =
            Arrays.asList(new RegraVolumePaoQueijo(), new RegraPesoPaoQueijo());
    private final List<Regra<PacotePaoQueijo, PacoteDefeituosoException>> regrasPacotePaoQueijo =
            Arrays.asList(new RegraQuantidadePaesPacote(), new RegraPesoPacote());


    public void validarPacote(PacotePaoQueijo pacote) {
        for (var regra : regrasPacotePaoQueijo) {
            try {
                regra.validar(pacote);
            } catch (PacoteDefeituosoException e) {
                log.error("Defeito no pacote.", e);
            }
        }

        for (PaoDeQueijo paoDeQueijo : pacote) {
            for (var regra : regrasPaoDeQueijo) {
                try {
                    regra.validar(paoDeQueijo);
                } catch (PaoDeQueijoDefeituosoException e) {
                    log.error("Defeito no pão de queijo.", e);
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Nossas regras implementam uma interface única, mas poderia haver regras para pacote e regras para pão de queijo separadas.

public interface Regra<T, E extends Exception> {
    void validar(T produto) throws E;
}
Enter fullscreen mode Exit fullscreen mode

Vamos ver aqui uma das regras, para verificação de peso do pacote.

public class RegraPesoPacote implements Regra<PacotePaoQueijo, PacoteDefeituosoException> {
    public static final float PESO_PACOTE = 500f;

    @Override
    public void validar(PacotePaoQueijo pacote) throws PacoteDefeituosoException {
        final double pesoPacote = pacote.stream().map(PaoDeQueijo::getPeso).reduce(0., Double::sum);
        if (pesoPacote == PESO_PACOTE)
            return;

        throw new PacoteDefeituosoException(String.format("O peso do pacote de pães de queijo não está correto. " +
                "Esperava %f g e encontrou %f g.", PESO_PACOTE, pesoPacote));
    }
}
Enter fullscreen mode Exit fullscreen mode

Todas as regras seguem esse estilo, uma classe simples, com o método validar que lança uma exceção caso ela não seja validada.
Observe como fica auto-contido todo o universo da regra!

Plus com injeção Spring

Eu não testei essa funcionalidade com outros injetores de dependências, mas sei que com o Spring funciona bem.

O Spring consegue de forma simples injetar uma lista com todas as classes que implementam uma interface. Assim, nossa esteira fica ainda mais simples.

@AllArgsConstructor
@Component
@Log4j
public class EsteiraAvaliacaoPaesDeQueijo {
    private final List<Regra<PaoDeQueijo, PaoDeQueijoDefeituosoException>> regrasPaoDeQueijo;
    private final List<Regra<PacotePaoQueijo, PacoteDefeituosoException>> regrasPacotePaoQueijo;

    public void validarPacote(PacotePaoQueijo pacote) {
        for (var regra : regrasPacotePaoQueijo) {
            try {
                regra.validar(pacote);
            } catch (PacoteDefeituosoException e) {
                log.error("Defeito no pacote.", e);
            }
        }

        for (PaoDeQueijo paoDeQueijo : pacote) {
            for (var regra : regrasPaoDeQueijo) {
                try {
                    regra.validar(paoDeQueijo);
                } catch (PaoDeQueijoDefeituosoException e) {
                    log.error("Defeito no pão de queijo.", e);
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Importante observação, não fique tentado a achar que se trata de um Chain of Responsabilities. Esse padrão prevê que uma responsabilidade está ligada à outra e compõem uma ordem definida. Já este não acopla as regras e, desta forma, se torna muito mais escalável. Outro ponto é que podemos criar regras que são reaproveitáveis, bastando que os objetos a validar possuam uma interface em comum.

O código desse projeto pode ser encontrado aqui.

. . . . . . .
Terabox Video Player