Comment remplacer plusieurs instructions if en Java

Comment remplacer plusieurs instructions if en Java

**

1. Vue d'ensemble

Les constructions de décision sont une partie vitale de tout langage de programmation. Mais nous arrivons à coder un grand nombre de déclarations imbriquées qui rendent notre code plus complexe et difficile à maintenir.

Dans ce didacticiel, nous allons parcourir lesvarious ways of replacing nested if statements.

Explorons différentes options pour simplifier le code.

2. Étude de cas

Nous rencontrons souvent une logique d’entreprise qui implique de nombreuses conditions, et chacune d’entre elles nécessite un traitement différent. Pour une démonstration, prenons l'exemple d'une classeCalculator. Nous aurons une méthode qui prend deux nombres et un opérateur en entrée et renvoie le résultat en fonction de l'opération:

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

Nous pouvons également l'implémenter en utilisant les instructionsswitch:

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

Dans un développement typique,the if statements may grow much bigger and more complex in nature. De plus,the switch statements do not fit well when there are complex conditions.

Un autre effet secondaire des constructions de décision imbriquées est qu'elles deviennent ingérables. Par exemple, si nous devons ajouter un nouvel opérateur, nous devons ajouter une nouvelle instruction if et implémenter l'opération.

3. Refactoring

Explorons les autres options pour remplacer les instructions if complexes ci-dessus en un code beaucoup plus simple et gérable.

3.1. Classe d'usine

Nous rencontrons souvent des constructions de décision qui finissent par effectuer la même opération dans chaque branche. Cela offre une opportunité àextract a factory method which returns an object of a given type and performs the operation based on the concrete object behavior.

Pour notre exemple, définissons une interfaceOperation qui a une seule méthodeapply:

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

La méthode prend deux nombres en entrée et renvoie le résultat. Définissons une classe pour effectuer des ajouts:

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

Nous allons maintenant implémenter une classe d'usine qui renvoie des instances deOperation en fonction de l'opérateur donné:

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

Maintenant, dans la classeCalculator, nous pouvons interroger la fabrique pour obtenir l'opération appropriée et appliquer sur les numéros source:

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

Dans cet exemple, nous avons vu comment la responsabilité est déléguée à des objets faiblement couplés servis par une classe fabrique. Mais il est possible que les déclarations imbriquées soient simplement déplacées vers la classe d'usine, ce qui va à l'encontre de notre objectif.

Alternativement,we can maintain a repository of objects in a Map which could be queried for a quick lookup. Comme nous l'avons vu,OperatorFactory#operationMap sert notre objectif. Nous pouvons également initialiserMap au moment de l'exécution et les configurer pour la recherche.

3.2. Utilisation d'énumérations

En plus de l'utilisation deMap,we can also use Enum to label particular business logic. Après cela, nous pouvons les utiliser dans lesif statements imbriqués ouswitch casestatements. Alternativement, nous pouvons également les utiliser comme une fabrique d'objets et les élaborer de stratégies pour exécuter la logique métier associée.

Cela réduirait également le nombre d'instructions if imbriquées et déléguerait la responsabilité à des valeursEnum individuelles.

Voyons comment nous pouvons y parvenir. Dans un premier temps, nous devons définir nosEnum:

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

Comme on peut le constater, les valeurs sont les étiquettes des différents opérateurs qui seront utilisées ultérieurement pour le calcul. Nous avons toujours la possibilité d'utiliser les valeurs comme conditions différentes dans les instructions if imbriquées ou les cas de commutation, mais concevons une autre façon de déléguer la logique auEnum lui-même.

Nous allons définir des méthodes pour chacune des valeursEnum et effectuer le calcul. Par exemple:

ADD {
    @Override
    public int apply(int a, int b) {
        return a + b;
    }
},
// other operators

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

Et puis dans la classeCalculator, nous pouvons définir une méthode pour effectuer l'opération:

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

Maintenant, nous pouvons invoquer la méthode parconverting the String value to the Operator by using the Operator#valueOf() method:

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

3.3. Modèle de commande

Dans la discussion précédente, nous avons vu l'utilisation de la classe fabrique pour renvoyer l'instance du bon objet métier pour l'opérateur donné. Plus tard, l'objet métier est utilisé pour effectuer le calcul dans lesCalculator.

We can also design a Calculator#calculate method to accept a command which can be executed on the inputs. Ce sera une autre façon de remplacer lesif statements imbriqués.

Nous allons d'abord définir notre interfaceCommand:

public interface Command {
    Integer execute();
}

Ensuite, implémentons unAddCommand:

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

Enfin, introduisons une nouvelle méthode dans lesCalculator qui accepte et exécute lesCommand:

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

Ensuite, nous pouvons invoquer le calcul en instanciant unAddCommand et l'envoyer à la méthodeCalculator#calculate:

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

3.4. Moteur de règles

Lorsque nous finissons par écrire un grand nombre d'instructions if imbriquées, chacune des conditions décrit une règle de gestion qui doit être évaluée pour que la logique correcte soit traitée. Un moteur de règles supprime cette complexité du code principal. RuleEngine evaluates the Rules and returns the result based on the input.

Voyons un exemple en concevant un simpleRuleEngine qui traite unExpression à travers un ensemble deRules et retourne le résultat desRule sélectionnés. Tout d'abord, nous allons définir une interfaceRule:

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

Deuxièmement, implémentons unRuleEngine:

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

LeRuleEngine accepte un objetExpression et renvoie lesResult. Maintenant, concevons la classeExpression comme un groupe de deux objetsInteger avec lesOperator qui seront appliqués:

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

Et enfin, définissons une classeAddRule personnalisée qui n'évalue que lorsque leADD Operation est spécifié:

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

Nous allons maintenant appeler lesRuleEngine avec unExpression:

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

Dans ce didacticiel, nous avons exploré différentes options pour simplifier un code complexe. Nous avons également appris à remplacer les instructions if imbriquées par l'utilisation de modèles de conception efficaces.

Comme toujours, nous pouvons trouver le code source complet sur lesGitHub repository.

**