替代 Java 中繁琐的 If 语句

案例分析

我们经常遇到涉及很多条件的业务逻辑,每个都需要不同的处理。我们以 Calculator 类为例。我们将有一个方法,它接受两个数字和一个运算符作为输入,并根据操作返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 语句实现:

1
2
3
4
5
6
7
8
9
public int calculateUsingSwitch(int a, int b, String operator) {
switch (operator) {
case "add":
result = a + b;
break;
// other cases
}
return result;
}

在典型的开发中,if 语句可能会变得更大,更复杂。此外,当存在复杂条件时,switch 语句不适合。

嵌套决策结构的另一个副作用是它们变得难以管理。例如,如果我们需要添加一个新的运算符,我们必须添加一个新的 if 语句并实现该操作。

重构

让我们探索替代选项,将上面的复杂if语句替换为更简单和易于管理的代码。

  • 工厂类

很多时候我们遇到决策结构,最终在每个分支中执行类似的操作。这提供了提取工厂方法的机会,该工厂方法返回给定类型的对象并基于具体对象行为执行操作。
对于上述用例,我们定义一个 Operation 接口,它仅有一个 apply 方法:

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

这个方法接受两个数字,输出结果。让我们定义一个类来执行加:

1
2
3
4
5
6
public class Addition implements Operation {
@override
public int apply(int a, int b) {
return a + b;
}
}

实现一个工厂类,它根据给定的运算符返回 Operation 的实例:

1
2
3
4
5
6
7
8
9
10
11
public class OperatorFactory {
static Map<String, Operation> operationMap = new HashMap<>();
static {
operationMap.put("add", new Addition());
operationMap.put("divide", new Division());
// more operations
}
public static Optional<Operation> getOperation(String operator) {
return Optional.ofNullable(operationMap.get(operator));
}
}

现在,在 Calculator 类中,我们可以查询工厂以获取相关操作并应用源数:

1
2
3
4
5
6
public int calculateUsingFactory(int a, int b, String operator) {
Operation targetOperation = OperatorFactory
.getOperation(operator)
.orElseThrow(() -> nwe IllegalArgumentException("Invalid Operator"));
return targetOperation.apply(a, b);
}

在这个例子中,我们已经看到了如何将责任委托给工厂类提供的松散耦合对象。但是有可能嵌套的 if 语句只是转移到了工厂类,这违背了我们的目的。
或者,我们可以在 Map 中维护一个对象存储库,可以查询该存储库以进行快速查找。

  • 使用枚举

    除了使用 Map 之外,我们还可以使用 Enum 来标记特定的业务逻辑。之后,我们可以在嵌套的 if 语句或 switch case 语句中使用它们。或者,我们也可以将它们用作对象的工厂并制定策略以执行相关的业务逻辑。
    这也会减少嵌套 if 语句的数量,并将责任委托给单个 Enum 值。
    让我们看如何实现它,首先,定义一个枚举:
    1
    2
    3
    public enum Operator {
    ADD, MULTIPLY, SUBTRACT, DIVIDE
    }
    我们可以观察到,这些值是不同运算符的标签,将进一步用于计算。我们总是可以选择在嵌套的 if 语句或 switch case
    中使用这些值作为不同的条件,
    但是让我们设计一种将逻辑委托给 Enum 本身的替代方法。 我们将为每个 Enum 值定义方法并进行计算。例如:
1
2
3
4
5
6
7
8
9
10
11
public enum Operator {
ADD {
@Override
public int apply(int a, int b) {
return a + b;
}
},
// other operators

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

这种写法称作 Constant-specific methods, 参考 枚举
然后在 Calculator 类中,我们可以定义一个方法来执行操作:

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

现在,我们可以通过使用 Operator#valueOf() 方法将String 值转换为 Operator 来调用该方法:

1
2
3
4
5
6
@Test
public void whenCalculateUsingEnumOperator_thenReturnCorrectResult() {
Calculator calculator = new Calculator();
int result = calculator.calculate(3, 4, Operator.valueOf("ADD"));
assertEquals(7, result);
}
  • 命令模式

    我们还可以设计一个 Calculator#calculate 方法来接受可以在输入上执行的命令。这将是替换嵌套 if 语句的另一种方法。
    首先定义我们的 Command 接口:
1
2
3
public interface Command {
Integer execute();
}

然后,我们实现一个 AddCommand:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
}

最后,我们在 Calculator 中添加一个接收和执行 Command 的新方法:

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

然后,我们可以通过实例化 AddCommand 调用计算并将其发送到 Calculator#calculate 方法:

1
2
3
4
5
6
@Test
public void whenCalculateUsingCommand_thenReturnCorrectResult() {
Calculator calculator = new Calculator();
int result = calculator.calculate(new AddCommand(3, 7));
assertEquals(10, result);
}
  • 规则引擎

当我们最终编写大量嵌套 if 语句时,每个条件都描述了一个业务规则,必须对其进行评估才能处理正确的逻辑。规则引擎从主代码中分离了这种复杂性。 RuleEngine 评估规则并根据输入返回结果。
让我们通过设计一个简单的 RuleEngine 来演示一个例子,该 RuleEngine 通过一组规则处理 Expression 并返回所选规则的结果。首先,我们将定义一个 Rule 接口:

1
2
3
4
public interface Rule {
boolean evaluate(Expression expression);
Result getResult();
}

其次,让我们实现一个 RuleEngine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 对象的 Operator,它将被应用:

1
2
3
4
5
public class Expression {
private Integer x;
private Integer y;
private Operator operator;
}

最后让我们定义一个自定义的 AddRule 类,它仅在指定 ADD 操作时进行求值:

1
2
3
4
5
6
7
8
9
10
11
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;
}
}

我们现在将使用 Expression 调用 RuleEngine :

1
2
3
4
5
6
7
8
9
@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());
}