Accueil > Non classé > Dé-switcher n’est pas jouer

Dé-switcher n’est pas jouer

Amis utilisateurs de Checkstyle, avez-vous remarqué que les switch sont des sources inépuisables de complexité cyclomatique ? Sans parler de leur équivalent pour les objets, les if () {} else if (){} else if (){} else if (){} à répétition.

Parmi les reproches récurrents faits aux switch, il y a le fait qu’ils ne respectent pas le principe Ouvert/Fermé. A chaque ajout de nouvelles valeurs, il faut modifier tous les switch qui les manipulent.


Il est toujours possible de se passer d’un switch. Un moyen simple est d’utiliser une Map. Ainsi, le code suivant :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public String getMessage(int messageCode) {
    String message;
    switch (messageCode) {
        case 42:
            message = "the Answer to the Ultimate Question of Life, the Universe, and Everything.";
            break;
        case 13:
            message = "Good luck!";
            break;
        // [...]
        default:
            throw new IllegalArgumentException("Unknown code");
    }
    return message;
}

devient :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final Map<Integer, String>  messagesByCode = new HashMap<Integer, String>();

private void initMessagesByCode() {
    messagesByCode.put(42, "the Answer to the Ultimate Question of Life, the Universe, and Everything.");
    messagesByCode.put(13, "Good luck!");
    // [...]
}

public String getMessage(int messageCode) {
    String message = messagesByCode.get(messageCode);

    if (message == null) {
        throw new IllegalArgumentException("Unknown code");
    }

    return message;
}

Cela fonctionne aussi si le switch (ou les if/else) contient du code à exécuter qui varie suivant les cas. Ainsi :


1
2
3
4
5
6
7
8
9
10
11
12
public void doAction(String action) {
    if ("syso".equals(action)) {
        System.out.println("Yeah");
    } else if ("syserr".equals(action)) {
        System.err.println("Yahoo");
    } // [...]
    else {
        throw new IllegalArgumentException("Unknown action");
    }
}
// [...]
doAction("syso");

devient (j’en conviens, des first-class functions simplifieraient le code) :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private final Map<String, Runnable> actionsByName = new HashMap<String, Runnable>();

private void initActionsByName() {
    actionsByName.put("syso", new Runnable() {
        public void run() {
            System.out.println("Yeah");
        }
    });
    actionsByName.put("syserr", new Runnable() {
        public void run() {
            System.err.println("Yahoo");
        }
    });
}

public void doAction(String action) {
    Runnable runnable = actionsByName.get(action);

    if (runnable == null) {
        throw new IllegalArgumentException("Unknown action");
    }

    runnable.run();
}
// [...]
doAction("syso");

Clairement, si ce type de modification permet de se passer de switch sans modifier la signature des méthodes, elle n’est pas idéale. Une bien meilleure option ici serait de se souvenir que Java est un langage orienté Objet, et utiliser le polymorphisme pour implémenter le pattern strategy ;-) :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface Action {
    void doAction();
}
// [...]
public void doAction(Action action) {
    action.doAction();
}
// [...]
public class Syso implements Action {
    public void doAction() {
        System.out.println("Yeah");
    }
}
// [...]
public class Syserr implements Action {
    public void doAction() {
        System.err.println("Yahoo");
    }
}
// [...]
doAction(new Syso());

On pourrait enfin utiliser les possibilités offertes par les enum Java :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum ActionEnum implements Action {
    SYSO {
        public void doAction() {
            System.out.println("Yeah");
        }
    },
    SYSERR {
        public void doAction() {
            System.err.println("Yahoo");
        }
    };
}
// [...]
doAction(ActionEnum.SYSO);

J’espère que ce court article vous aura convaincu qu’il est tout à fait possible de se passer des switch. Un code comprenant des switch et autres if () {} else if (){} est en général tout sauf simple et maintenable.
Dans Clean Code, Uncle Bob Martin conseille d’ailleurs d’encapsuler le code lié aux switch dans des fabriques abstraites, afin de ne pas les laisser polluer le reste du code.

Edit (03/08/2010) : dans le même domaine, vous pouvez lire cet article sur le Refactoring par la pratique.

Share
  1. Raphaël LEMAIRE
    25/06/2010 à 09:38 | #1

    Bon article.

    On attend avec impatience de pouvoir améliorer l’astuce de la Map avec des closures comme valeurs.

    Il existe un site contre les ifs qui devraient être du polymorphisme :

    http://www.antiifcampaign.com/

  2. 25/06/2010 à 11:27 | #2

    Doublé par Raphaël, je voulais faire référence à http://www.antiifcampaign.com/

  3. ParrainGeek31SansIPhone
    25/06/2010 à 16:46 | #3

    C’est une transmission de pensée(s), je viens de remplacer un gros bloc switch case par
    final EnumMap sortTranslator
    = new EnumMap(SortEnum.class);
    sortTranslator.put(SortEnum.REFERENCE, “a.reference”);
    // etc….
    Et mon filleul vient de m’envoyer le lien de ton site !
    PS: noter que j’ai fait une EnumMap et pas une Map pour éviter de me baser sur le equals/hashCode car je travaille avec des enum.

  4. Neko
    01/07/2010 à 12:12 | #4

    Salut à toi Pierre Yves (salut aux autres lecteurs)

    Je ne suis absolument pas d’accord avec ton article (enfin en partie).
    Tout d’abord, si le switch existe depuis si longtemps, c’est peut être parce que c’est plus qu’une structure de contrôle; je veux dire, des optimisations sont faites derrière par le compilateur. (Certes, le Java est un cas légèrement différent de part sa VM, et je ne sais pas programmer dans ce dernier, mais je pense que mon post est quand même valable)

    Actuellement, un swich avec beaucoup de cas différents est automatiquement remplacé (quand c’est possible) par un pointeur sur fonctions, justement pour éviter une batterie de if/else.

    Je t’invite à lire cet article http://www.safercode.com/blog/2008/10/28/optimizing-switch-case-statements-in-c-for-speed.html qui parle de l’optimisation des switch/case en C.

    Ensuite je voudrais parler de ton point sur le pattern “strategy”.
    C’est un choix dangereux, car si ta classe est lourde, tu n’as pas forcément envie d’en faire X sous classes pour implémenter quelques stratégies, je pense donc que ce genre de patterns est à utiliser avec parcimonie.

    De manière générale, il ne faut pas diaboliser les structures de contrôles if/else et switch.
    Je pense qu’il serait plus approprié d’apprendre à les utiliser avec parcimonie, et surtout efficacité, car ils ont tous des avantages/désavantages selon les situations.

    Un code contenant des switch et if/else n’est pas forcément difficile à maintenir (même si, ça à souvent l’air d’être le cas), ou compliqué à comprendre.

    Voila, bonne journée à toi.

  5. 01/07/2010 à 21:51 | #5

    Bonjour,

    Autant je comprends et approuve fortement la première partie de l’article, autant je ne comprends pas l’utilité du pattern strategy dans ce contexte.

    Quelle différence entre doAction(new Syso()); et new Syso().doAction(); ?

    Le switch permet de relier un ensemble de valeurs à un ensemble d’exécutions. Si on dispose déjà de l’exécution, on n’est plus tellement dans le cadre d’un switch.

    Ce billet m’a toutefois donné envie d’exposer une technique que j’utilise parfois en C# pour éviter la maintenance manuelle d’un “Map” centralisé : http://agilitateur.azeau.com/post/2010/07/01/Un-switch-%C3%A9volutif-en-C
    Cela se base sur le concept d’attributs dans .NET
    Je connais mal java mais est-ce que la gestion des “case” d’un “switch” via des annotations y serait possible ? Et aurait elle un intérêt ?

    Concrètement, est-ce que l’on pourrait écrire quelque chose comme (syntaxe non garantie) :

    @Case(“syso”)
    public class Syso implements Action {
    public void doAction() { System.out.println(“Yeah”); }
    }

    @Case(“syserr”)
    public class Syserr implements Action {
    public void doAction() { System.err.println(“Yahoo”); }
    }

    et avoir un mécanisme générique qui, étant donné une string “syso” ou “syserr” exécuterait le “doAction” correspondant ?

  6. 02/07/2010 à 12:11 | #6

    @Oaz
    Décidément, cet article semble déchaîner les passions dans les commentaires :-D ! Dommage que Pierre-Yves ne puisse pas répondre à cause d’une vacancite aigüe…
    Ton idée m’a donné envie de tester la faisabilité d’un @Case en Java, et bien évidemment c’est possible. J’ai documenté ça sur un autre blog : http://foobaz.free.fr/blog/?article7/un-switch-annote-en-java

  7. Denis VAUMORON
    07/07/2010 à 21:07 | #7

    Le @Case est pas mal, mais en casse-pieds de service, je note tout de même que tu aurais pu utiliser la méthode asSubclass pour ta méthode isA :

    private boolean isA(Class clazz, Class class1) {
    try {
    clazz.asSubclass(class1);
    } catch (ClassCastException cce) {
    return false;
    }
    return true;
    }

  8. 10/07/2010 à 11:56 | #8

    Mouais, en râleur de service je trouve ça moyennement “propre” d’appeler une méthode en sachant qu’elle peut me jeter une exception dans la face, même si je l’attrape… Mais pour le coup j’admets que ça simplifie le code : on n’a pas besoin de tester les deux types d’extension, à savoir super-classe ou interfaces.

  1. 25/06/2010 à 15:22 | #1
  2. 01/07/2010 à 10:55 | #2
  3. 03/08/2010 à 14:50 | #3
  4. 21/02/2011 à 10:46 | #4
  5. 05/12/2011 à 13:09 | #5
  6. 15/05/2015 à 07:58 | #6
  7. 26/05/2015 à 00:58 | #7
  8. 01/07/2015 à 23:08 | #8
  9. 02/07/2015 à 01:36 | #9
  10. 06/08/2015 à 17:51 | #10
  11. 11/08/2015 à 01:09 | #11
  12. 02/09/2015 à 23:30 | #12
  13. 03/09/2015 à 08:11 | #13
  14. 03/09/2015 à 11:15 | #14
  15. 04/10/2015 à 17:33 | #15
  16. 30/10/2015 à 20:17 | #16