Como substituir muitas instruções se em Java

Como substituir muitas instruções se em Java

**

1. Visão geral

As construções de decisão são uma parte vital de qualquer linguagem de programação. Mas acabamos codificando um grande número de instruções if aninhadas que tornam nosso código mais complexo e difícil de manter.

Neste tutorial, vamos percorrer osvarious ways of replacing nested if statements.

Vamos explorar as diferentes opções de como podemos simplificar o código.

2. Estudo de caso

Muitas vezes, encontramos uma lógica comercial que envolve muitas condições, e cada uma delas precisa de processamento diferente. Para fins de demonstração, vamos pegar o exemplo de uma classeCalculator. Teremos um método que recebe dois números e um operador como entrada e retorna o resultado com base na operação:

public int calculate(int a, int b, String operator) {
    int result = Integer.MIN_VALUE;

    if ("add".equals(operator)) {
        result = a + b;
    } else if ("multiply".equals(operator)) {
        result = a * b;
    } else if ("divide".equals(operator)) {
        result = a / b;
    } else if ("subtract".equals(operator)) {
        result = a - b;
    }
    return result;
}

Também podemos implementar isso usando instruçõesswitch:

public int calculateUsingSwitch(int a, int b, String operator) {
    switch (operator) {
    case "add":
        result = a + b;
        break;
    // other cases
    }
    return result;
}

Em desenvolvimento típico,the if statements may grow much bigger and more complex in nature. Além disso,the switch statements do not fit well when there are complex conditions.

Outro efeito colateral de ter construções de decisão aninhadas é que elas se tornam incontroláveis. Por exemplo, se precisarmos adicionar um novo operador, precisamos adicionar uma nova instrução if e implementar a operação.

3. Reestruturação

Vamos explorar as opções alternativas para substituir as complexas instruções if acima em um código muito mais simples e gerenciável.

3.1. Classe de fábrica

Muitas vezes encontramos construções de decisão que acabam fazendo a operação semelhante em cada ramo. Isso fornece uma oportunidade paraextract a factory method which returns an object of a given type and performs the operation based on the concrete object behavior.

Para nosso exemplo, vamos definir uma interfaceOperation que tem um único métodoapply:

public interface Operation {
    int apply(int a, int b);
}

O método usa dois números como entrada e retorna o resultado. Vamos definir uma classe para realizar adições:

public class Addition implements Operation {
    @Override
    public int apply(int a, int b) {
        return a + b;
    }
}

Agora vamos implementar uma classe de fábrica que retorna instâncias deOperation com base no operador fornecido:

public class OperatorFactory {
    static Map operationMap = new HashMap<>();
    static {
        operationMap.put("add", new Addition());
        operationMap.put("divide", new Division());
        // more operators
    }

    public static Optional getOperation(String operator) {
        return Optional.ofNullable(operationMap.get(operator));
    }
}

Agora, na classeCalculator, podemos consultar a fábrica para obter a operação relevante e aplicar nos números de origem:

public int calculateUsingFactory(int a, int b, String operator) {
    Operation targetOperation = OperatorFactory
      .getOperation(operator)
      .orElseThrow(() -> new IllegalArgumentException("Invalid Operator"));
    return targetOperation.apply(a, b);
}

Neste exemplo, vimos como a responsabilidade é delegada a objetos fracamente acoplados servidos por uma classe de fábrica. Mas pode haver chances de as instruções if aninhadas serem simplesmente deslocadas para a classe factory, o que derrota nosso propósito.

Alternativamente,we can maintain a repository of objects in a Map which could be queried for a quick lookup. Como vimos,OperatorFactory#operationMap atende ao nosso propósito. Também podemos inicializarMap em tempo de execução e configurá-los para pesquisa.

3.2. Uso de Enums

Além do uso deMap,we can also use Enum to label particular business logic. Depois disso, podemos usá-los no aninhadoif statements ouswitch casestatements. Como alternativa, também podemos usá-los como uma fábrica de objetos e estruturá-los para executar a lógica de negócios relacionada.

Isso também reduziria o número de instruções if aninhadas e delegaria a responsabilidade aos valoresEnum individuais.

Vamos ver como podemos alcançá-lo. Primeiramente, precisamos definir nossoEnum:

public enum Operator {
    ADD, MULTIPLY, SUBTRACT, DIVIDE
}

Como podemos observar, os valores são os rótulos dos diferentes operadores que serão usados ​​ainda mais para o cálculo. Sempre temos a opção de usar os valores como condições diferentes em instruções if aninhadas ou casos de troca, mas vamos projetar uma forma alternativa de delegar a lógica ao próprioEnum.

Vamos definir métodos para cada um dos valores deEnum e fazer o cálculo. Por exemplo:

ADD {
    @Override
    public int apply(int a, int b) {
        return a + b;
    }
},
// other operators

public abstract int apply(int a, int b);

E então, na classeCalculator, podemos definir um método para realizar a operação:

public int calculate(int a, int b, Operator operator) {
    return operator.apply(a, b);
}

Agora, podemos invocar o método porconverting the String value to the Operator by using the Operator#valueOf() method:

@Test
public void whenCalculateUsingEnumOperator_thenReturnCorrectResult() {
    Calculator calculator = new Calculator();
    int result = calculator.calculate(3, 4, Operator.valueOf("ADD"));
    assertEquals(7, result);
}

3.3. Padrão de comando

Na discussão anterior, vimos o uso da classe factory para retornar a instância do objeto de negócios correto para o operador especificado. Posteriormente, o objeto de negócio é usado para realizar o cálculo noCalculator.

We can also design a Calculator#calculate method to accept a command which can be executed on the inputs. Essa será outra maneira de substituirif statements aninhados.

Vamos primeiro definir nossa interfaceCommand:

public interface Command {
    Integer execute();
}

A seguir, vamos implementar umAddCommand:

public class AddCommand implements Command {
    // Instance variables

    public AddCommand(int a, int b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer execute() {
        return a + b;
    }
}

Finalmente, vamos introduzir um novo método emCalculator que aceita e executa oCommand:

public int calculate(Command command) {
    return command.execute();
}

A seguir, podemos invocar o cálculo instanciando umAddCommande enviá-lo para o métodoCalculator#calculate:

@Test
public void whenCalculateUsingCommand_thenReturnCorrectResult() {
    Calculator calculator = new Calculator();
    int result = calculator.calculate(new AddCommand(3, 7));
    assertEquals(10, result);
}

3.4. Motor de Regras

Quando acabamos escrevendo um grande número de instruções if aninhadas, cada uma das condições representa uma regra de negócios que deve ser avaliada para que a lógica correta seja processada. Um mecanismo de regras remove essa complexidade do código principal. RuleEngine evaluates the Rules and returns the result based on the input.

Vamos examinar um exemplo projetando umRuleEngine simples que processa umExpression por meio de um conjunto deRulese retorna o resultado doRule selecionado. Primeiro, vamos definir uma interfaceRule:

public interface Rule {
    boolean evaluate(Expression expression);
    Result getResult();
}

Em segundo lugar, vamos implementar umRuleEngine:

public class RuleEngine {
    private static List rules = new ArrayList<>();

    static {
        rules.add(new AddRule());
    }

    public Result process(Expression expression) {
        Rule rule = rules
          .stream()
          .filter(r -> r.evaluate(expression))
          .findFirst()
          .orElseThrow(() -> new IllegalArgumentException("Expression does not matches any Rule"));
        return rule.getResult();
    }
}

ORuleEngine aceita um objetoExpression e retornaResult. Agora, vamos projetar a classeExpression como um grupo de dois objetosInteger com osOperator que serão aplicados:

public class Expression {
    private Integer x;
    private Integer y;
    private Operator operator;
}

E, finalmente, vamos definir uma classeAddRule personalizada que avalia apenas quandoADD Operation é especificado:

public class AddRule implements Rule {
    @Override
    public boolean evaluate(Expression expression) {
        boolean evalResult = false;
        if (expression.getOperator() == Operator.ADD) {
            this.result = expression.getX() + expression.getY();
            evalResult = true;
        }
        return evalResult;
    }
}

Agora invocaremosRuleEngine com umExpression:

@Test
public void whenNumbersGivenToRuleEngine_thenReturnCorrectResult() {
    Expression expression = new Expression(5, 5, Operator.ADD);
    RuleEngine engine = new RuleEngine();
    Result result = engine.process(expression);

    assertNotNull(result);
    assertEquals(10, result.getValue());
}

4. Conclusão

Neste tutorial, exploramos várias opções diferentes para simplificar o código complexo. Também aprendemos como substituir instruções if aninhadas pelo uso de padrões de design eficazes.

Como sempre, podemos encontrar o código-fonte completo emGitHub repository.

**