Как да замените много, ако изявления в Java

1. Общ преглед

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

В този урок ще разгледаме различните начини за заместване на вложени оператори if .

Нека разгледаме различни опции как можем да опростим кода.

2. Казус

Често срещаме бизнес логика, която включва много условия и всяко от тях се нуждае от различна обработка. В името на демонстрацията, нека вземем примера на клас Калкулатор . Ще имаме метод, който приема две числа и оператор като вход и връща резултата въз основа на операцията:

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

Можем да приложим това и с помощта на инструкции за превключване :

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

В типичното развитие, ако изявленията могат да станат много по-големи и по-сложни по своя характер . Освен това инструкциите за превключване не се вписват добре, когато има сложни условия .

Друг страничен ефект от наличието на вложени конструкции за решения е, че те стават неуправляеми. Например, ако трябва да добавим нов оператор, трябва да добавим нов оператор if и да приложим операцията.

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

Нека да разгледаме алтернативните опции за заместване на сложните ако твърдения по-горе в много по-опростен и управляем код.

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

Сега ще внедрим фабричен клас, който връща екземпляри на 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)); } }

Сега, в класа на калкулатора , можем да направим запитване към фабриката, за да получим съответната операция и да приложим на номера на източника:

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

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

Като алтернатива можем да поддържаме хранилище на обекти в Карта, което може да бъде поискано за бързо търсене . Както видяхме, OperatorFactory # operationMap служи за нашата цел. Също така можем да инициализираме Map по време на изпълнение и да ги конфигурираме за търсене.

3.2. Използване на Enums

В допълнение към използването на Map, ние също можем да използваме Enum за обозначаване на конкретна бизнес логика . След това, ние можем да ги използвате или в вложени ако изявления или превключвател случай изявления . Като алтернатива можем да ги използваме и като фабрика на обекти и да ги стратегиираме за изпълнение на свързаната бизнес логика.

Това би намалило и броя на вложените оператори 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);

И тогава в класа Калкулатор можем да дефинираме метод за извършване на операцията:

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

Сега, ние може да се позове на метода, по превръщане на String стойност на оператора с помощта на оператора # valueOf () метода :

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

3.3. Команден модел

В предишната дискусия видяхме използването на фабричен клас за връщане на екземпляра на правилния бизнес обект за дадения оператор. По-късно бизнес обектът се използва за извършване на изчислението в калкулатора .

Също така можем да проектираме метод за калкулатор # за изчисляване , за да приеме команда, която може да бъде изпълнена на входовете . Това ще бъде друг начин за замяна на вложени оператори 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(); }

Next, we can invoke the calculation by instantiating an AddCommand and send it to the Calculator#calculate method:

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

3.4. Rule Engine

When we end up writing a large number of nested if statements, each of the conditions depicts a business rule which has to be evaluated for the correct logic to be processed. A rule engine takes such complexity out of the main code. A RuleEngine evaluates the Rules and returns the result based on the input.

Let's walk through an example by designing a simple RuleEngine which processes an Expression through a set of Rules and returns the result from the selected Rule. First, we'll define a Rule interface:

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

Second, let's implement a RuleEngine:

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

The RuleEngine accepts an Expression object and returns the Result. Now, let's design the Expression class as a group of two Integer objects with the Operator which will be applied:

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.