Рубрики
Без рубрики

Как заменить многие операторы if в Java

Изучите различные способы замены сложных вложенных операторов if

Автор оригинала: baeldung.

1. Обзор

Конструкции принятия решений являются жизненно важной частью любого языка программирования. Но мы попадаем в кодирование огромного количества вложенных операторов if, которые делают наш код более сложным и трудным в обслуживании.

В этом уроке мы рассмотрим различные способы замены вложенных операторов if .

Давайте рассмотрим различные варианты того, как мы можем упростить код.

2. Тематическое исследование

Часто мы сталкиваемся с бизнес-логикой, которая включает в себя множество условий, и каждое из них требует различной обработки. Для демонстрации давайте возьмем пример класса Calculator . У нас будет метод, который принимает два числа и оператор в качестве входных данных и возвращает результат на основе операции:

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;
}

Мы также можем реализовать это с помощью switch statements :

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

В типичном развитии утверждения if могут стать намного больше и сложнее по своей природе . Кроме того, операторы switch плохо подходят для сложных условий .

Еще одним побочным эффектом вложенных конструкций решений является то, что они становятся неуправляемыми. Например, если нам нужно добавить новый оператор, мы должны добавить новый оператор if и реализовать операцию.

3. Рефакторинг

Давайте рассмотрим альтернативные варианты замены сложных операторов if выше на гораздо более простой и управляемый код.

3.1. Заводской класс

Много раз мы сталкиваемся с конструкциями решений, которые в конечном итоге выполняют аналогичную операцию в каждой ветви. Это дает возможность извлечь фабричный метод, который возвращает объект заданного типа и выполняет операцию на основе поведения конкретного объекта .

Для нашего примера давайте определим интерфейс Operation , который имеет один apply метод:

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

Метод принимает два числа в качестве входных данных и возвращает результат. Давайте определим класс для выполнения дополнений:

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

Теперь мы реализуем фабричный класс, который возвращает экземпляры Operation на основе данного оператора:

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

Теперь в классе Calculator мы можем запросить завод, чтобы получить соответствующую операцию и применить исходные номера:

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

В этом примере мы видели, как ответственность делегируется слабо связанным объектам, обслуживаемым фабричным классом. Но могут быть шансы, что вложенные операторы if просто будут перенесены в класс factory, что противоречит нашей цели.

В качестве альтернативы, мы можем поддерживать репозиторий объектов в Карте , который может быть запрошен для быстрого поиска . Как мы видели, Operator Factory#operation Map служит нашей цели. Мы также можем инициализировать Map во время выполнения и настроить их для поиска.

3.2. Использование перечислений

В дополнение к использованию Map, мы также можем использовать Enum для обозначения конкретной бизнес-логики . После этого мы можем использовать их либо во вложенных операторах if , либо в операторах |//switch case |//. Кроме того, мы также можем использовать их в качестве фабрики объектов и разрабатывать стратегию для выполнения соответствующей бизнес-логики.

Это также уменьшило бы количество вложенных операторов if и делегировало бы ответственность отдельным значениям Enum .

Давайте посмотрим, как мы можем этого достичь. Сначала нам нужно определить наше Перечисление :

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

Как мы можем наблюдать, значения являются метками различных операторов, которые будут использоваться в дальнейшем для расчета. У нас всегда есть возможность использовать значения в качестве различных условий во вложенных операторах if или случаях переключения, но давайте разработаем альтернативный способ делегирования логики самому перечислению .

Мы определим методы для каждого из значений Enum и выполним расчет. Например:

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

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

А затем в классе Calculator мы можем определить метод для выполнения операции:

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

Теперь мы можем вызвать метод путем преобразования String значения в Оператор с помощью Operator#valueOf() метод :

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

3.3. Шаблон команд

В предыдущем обсуждении мы видели использование класса factory для возврата экземпляра правильного бизнес-объекта для данного оператора. Позже бизнес-объект используется для выполнения расчета в Калькуляторе .

Мы также можем разработать метод калькулятора#calculate , чтобы принять команду, которая может быть выполнена на входах . Это будет еще один способ замены вложенных операторов if .

Сначала мы определим наш Командный интерфейс:

public interface Command {
    Integer execute();
}

Далее, давайте реализуем команду Add:

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;
    }
}

Наконец, давайте представим новый метод в Калькуляторе , который принимает и выполняет команду :

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

Затем мы можем вызвать вычисление, создав экземпляр команды Add и отправив ее в метод Calculator#calculate :

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

3.4. Механизм правил

Когда мы в конечном итоге пишем большое количество вложенных операторов if, каждое из условий отображает бизнес-правило, которое должно быть оценено для правильной обработки логики. Механизм правил выводит такую сложность из основного кода. Механизм Правил оценивает Правила и возвращает результат на основе входных данных.

Давайте рассмотрим пример, разработав простой механизм Правил , который обрабатывает Выражение через набор Правил и возвращает результат из выбранного Правила . Во-первых, мы определим Правило интерфейс:

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

Во-вторых, давайте реализуем Механизм правил :

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();
    }
}

Механизм Правил принимает Выражение объекта и возвращает Результат . Теперь , давайте спроектируем класс Expression как группу из двух Целых объектов с оператором , который будет применен:

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

И, наконец, давайте определим пользовательский класс addRule , который вычисляется только тогда, когда указана операция ADD :

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;
    }    
}

Теперь мы вызовем механизм правил с выражением |:

@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. Заключение

В этом уроке мы рассмотрели ряд различных вариантов упрощения сложного кода. Мы также узнали, как заменить вложенные операторы if с помощью эффективных шаблонов проектирования.

Как всегда, мы можем найти полный исходный код в репозитории GitHub .