Перейти к основному содержимому

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

· 7 мин. чтения

Задача: Наибольшая подстрока без повторений

Для заданной строки s, найдите длину наибольшей подстроки без повторяющихся символов. Подстрока — это непрерывная непустая последовательность символов внутри строки...

ANDROMEDA 42

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 :

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. Заводской класс

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

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

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

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

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

public static Optional<Operation> 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 просто переместятся в фабричный класс, что противоречит нашей цели.

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

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

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

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

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

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

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

Мы определим методы для каждого из значений 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. Шаблон команды

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

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

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

public interface Command {
Integer execute();
}

Далее давайте реализуем AddCommand:

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

Затем мы можем вызвать вычисление, создав экземпляр AddCommand и отправив его в метод 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, каждое из условий отображает бизнес-правило, которое необходимо оценить для обработки правильной логики. Механизм правил избавляет основной код от такой сложности. RuleEngine оценивает правила и возвращает результат на основе ввода.

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

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

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

public class RuleEngine {
private static List<Rule> 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();
}
}

RuleEngine принимает объект Expression и возвращает Result . Теперь давайте разработаем класс Expression как группу из двух объектов Integer с оператором , который будет применяться:

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

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

@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 .