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 d'instructions if imbriquées qui rendent notre code plus complexe et difficile à maintenir.

Dans ce didacticiel, nous allons parcourir les différentes façons de remplacer les instructions if imbriquées .

Explorons différentes options pour simplifier le code.

2. Étude de cas

Nous rencontrons souvent une logique métier qui implique de nombreuses conditions, et chacune d'elles nécessite un traitement différent. Pour une démonstration, prenons l'exemple d'une classe Calculator . Nous aurons une méthode qui prend deux nombres et un opérateur en entrée et retourne 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 implémenter cela en utilisant des instructions switch :

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, les instructions if peuvent devenir beaucoup plus volumineuses et de nature plus complexe . De plus, les instructions switch ne correspondent pas bien lorsqu'il existe des conditions complexes .

Un autre effet secondaire d'avoir 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

Plusieurs fois, nous rencontrons des constructions de décision qui finissent par effectuer l'opération similaire dans chaque branche. Cela permet d' extraire une méthode de fabrique qui renvoie un objet d'un type donné et effectue l'opération en fonction du comportement concret de l'objet .

Pour notre exemple, définissons une interface Operation qui a une seule méthode apply :

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 retourne des instances d' Opération 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 classe Calculator , nous pouvons interroger l'usine 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 de fabrique. Mais il peut y avoir des chances que les instructions imbriquées if soient simplement déplacées vers la classe d'usine, ce qui va à l'encontre de notre objectif.

Alternativement, nous pouvons maintenir un référentiel d'objets dans une carte qui pourrait être interrogé pour une recherche rapide . Comme nous l'avons vu, OperatorFactory # operationMap sert notre objectif. Nous pouvons également initialiser Map au moment de l'exécution et les configurer pour la recherche.

3.2. Utilisation d'énumérations

En plus de l'utilisation de Map, nous pouvons également utiliser Enum pour étiqueter une logique métier particulière . Après cela, nous pouvons les utiliser soit dans le imbriquée si les déclarations ou cas de commutation déclarations . Alternativement, nous pouvons également les utiliser comme une fabrique d'objets et les élaborer des 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 valeurs Enum individuelles .

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

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

Comme nous pouvons 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 des instructions if imbriquées ou des cas de commutation, mais concevons une autre façon de déléguer la logique à Enum lui-même.

Nous définirons des méthodes pour chacune des valeurs Enum et ferons 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 classe Calculator , 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 appeler la méthode par la conversion de la chaîne de la valeur à l' opérateur en utilisant l' opérateur # valueOf () méthode :

@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 de fabrique pour renvoyer l'instance de l'objet métier correct pour l'opérateur donné. Plus tard, l'objet métier est utilisé pour effectuer le calcul dans la calculatrice .

Nous pouvons également concevoir une méthode de calcul Calculator # pour accepter une commande qui peut être exécutée sur les entrées . Ce sera une autre façon de remplacer les instructions if imbriquées .

Nous allons d'abord définir notre interface de commande :

public interface Command { Integer execute(); }

Ensuite, implémentons un 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; } }

Enfin, introduisons une nouvelle méthode dans la calculatrice qui accepte et exécute la commande :

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

Et enfin définissons une classe AddRule personnalisée qui n'évalue que lorsque l' opération ADD est spécifiée:

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 le RuleEngine avec une expression :

@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é un certain nombre d'options différentes 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 le référentiel GitHub.