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 MapoperationMap = 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 Listrules = 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 .