Validação de regras: evitando blocos de ifs
Tags: designpatterns, java, beginners, tutorial
Published: 2020-07-31 19:29:33

## O Problema :exploding_head:

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*][1]. 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
1. Peso da embalagem
1. Volume de cada pão de queijo
1. Peso de cada pão de queijo

Nossa principal entidade é pão de queijo.
```java
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);
    }
}
```

Um pacote de pães de queijo nada mais é do que uma lista.
```java 
public final class PacotePaoQueijo extends ArrayList<PaoDeQueijo> {
}
```

## Solução com vergonha :disappointed:

```java
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));
    }
}
```

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 :raised_hands:
```java
@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);
                }
            }
        }
    }
}
```
Nossas regras implementam uma interface única, mas poderia haver regras para pacote e regras para pão de queijo separadas.

```java
public interface Regra<T, E extends Exception> {
    void validar(T produto) throws E;
}
```

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

```java
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));
    }
}
```

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.

```java
@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);
                }
            }
        }
    }
}
```

Importante observação, não fique tentado a achar que se trata de um [*Chain of Responsabilities*][2]. 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][3].

[1]: https://en.wikipedia.org/wiki/SOLID
[2]: https://pt.wikipedia.org/wiki/Chain_of_Responsibility
[3]: https://github.com/leandrostl/validacao-regras/tree/master