1. 概要

意思決定構造は、プログラミング言語の重要な部分です。 しかし、コードをより複雑にし、保守を困難にする、ネストされたifステートメントを大量にコーディングすることになります。

このチュートリアルでは、ネストされたifステートメントを置き換えるさまざまな方法について説明します。

コードを単純化する方法について、さまざまなオプションを調べてみましょう。

2. ケーススタディ

多くの場合、多くの条件を含むビジネスロジックに遭遇し、それぞれが異なる処理を必要とします。 デモのために、Calculatorクラスの例を見てみましょう。 2つの数値と1つの演算子を入力として受け取り、操作に基づいて結果を返すメソッドがあります。

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. ファクトリクラス

多くの場合、各ブランチで同様の操作を実行する決定構造に遭遇します。 これは、特定のタイプのオブジェクトを返し、具体的なオブジェクトの動作に基づいて操作を実行するファクトリメソッドを抽出する機会を提供します。

この例では、単一のapplyメソッドを持つOperationインターフェースを定義しましょう。

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

このメソッドは、入力として2つの数値を受け取り、結果を返します。 追加を実行するためのクラスを定義しましょう。

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

ここで、指定された演算子に基づいてOperationのインスタンスを返すファクトリクラスを実装します。

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ステートメントまたはswitchケースで値を異なる条件として使用するオプションは常にありますが、ロジックを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);
}

これで、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で計算を実行します。

入力で実行できるコマンドを受け入れるように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 クラスを、適用されるOperatorを使用して2つのIntegerオブジェクトのグループとして設計しましょう。

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

最後に、 ADDOperationが指定されている場合にのみ評価されるカスタムAddRuleクラスを定義しましょう。

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を呼び出します。

@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リポジトリで見つけることができます。