Skip to content

Articles

Examen du cas en faveur des instructions switch

6 décembre 2015 • 11 min de lecture

Examen du cas en faveur des instructions switch

Pendant près de 50 ans, l’instruction switch (également connue sous le nom d’instruction case) a été une partie intégrante de la programmation. Ces dernières années, cependant, certains affirment que l’instruction switch a dépassé son utilité. D’autres vont encore plus loin en qualifiant l’instruction switch de code-smell.

En 1952, Stephen Kleene a conçu l’instruction switch dans son article, Introduction to Metamathematics. La première implémentation notable a été dans ALGOL 58 en 1958. Plus tard, l’instruction switch a été incluse dans le langage de programmation C indélébile, qui, comme nous le savons, a influencé la plupart des langages de programmation modernes.

En avançant jusqu’à nos jours, pratiquement tous les langages ont une instruction switch. Cependant, quelques langages ont omis l’instruction switch. Le plus notable étant Smalltalk.

Cela a piqué ma curiosité, pourquoi l’instruction switch a-t-elle été exclue de Smalltalk ?

Andy Bower, l’un des créateurs/partisans de Dolphin Smalltalk a partagé ses réflexions sur les raisons pour lesquelles Smalltalk a exclu l’instruction switch :

Quand je suis venu pour la première fois à Smalltalk depuis C++, je ne pouvais pas comprendre comment un langage supposément complet ne supportait pas une construction switch/case. Après tout, quand j’ai d’abord passé à la « programmation structurée » depuis BASIC, j’ai pensé que switch était l’une des meilleures choses depuis le pain de mie. Cependant, parce que Smalltalk ne supportait pas un switch, j’ai dû chercher et comprendre comment surmonter cette déficience. La bonne réponse est, bien sûr, d’utiliser le polymorphisme et de faire en sorte que les objets eux-mêmes se distribuent au bon morceau de code. Alors j’ai réalisé que ce n’était pas une « déficience » du tout, mais que Smalltalk me forçait à une conception OOP beaucoup plus fine que celle à laquelle j’avais l’habitude en C++. S’il y avait eu une instruction switch disponible, cela m’aurait pris beaucoup plus longtemps pour apprendre cela ou, pire, je pourrais encore programmer en style pseudo-objet C++/Java dans Smalltalk.
Je soutiendrais que dans la POO normale, il n’y a pas de réel besoin pour une instruction switch. Parfois, lors de l’interface avec un monde non-POO (comme la réception et la distribution de messages Windows WM_XXXX qui ne sont pas des objets mais juste des entiers), une instruction switch serait utile. Dans ces situations, il existe des alternatives (comme la distribution à partir d’un Dictionary) et le nombre de fois où elles se produisent ne justifie pas l’inclusion de syntaxe supplémentaire.

Andy avait-il raison ? Serions-nous mieux sans l’instruction switch ? D’autres langages bénéficieraient-ils également de l’exclusion de l’instruction switch ?

Pour éclairer cette question, j’ai rassemblé une comparaison entre une instruction switch, un dictionnaire et le polymorphisme. Appelons cela un affrontement. Que la meilleure implémentation gagne !

Chaque implémentation a une méthode prenant un paramètre, un entier, et retournant une chaîne. Nous utiliserons la complexité cyclomatique et l’indice de maintenabilité pour examiner chaque implémentation. Nous prendrons ensuite une vue holistique des trois implémentations.

Le code.

Instruction Switch

Indice de maintenabilité72
Complexité cyclomatique6
    public class SwitchWithFourCases
    {
        public string SwitchStatment(int color)
        {
            var colorString = "Red";

            switch (color)
            {
                case 1:
                    colorString = "Green";
                    break;

                case 2:
                    colorString = "Blue";
                    break;

                case 3:
                    colorString = "Violet";
                    break;

                case 4:
                    colorString = "Orange";
                    break;

            }

            return colorString;
        }
    }

Dictionnaire

Indice de maintenabilité73
Complexité cyclomatique3
public class DictionaryWithFourItems
{ 
    public string Dictionary(int color)
    {
        var colorString = "Red";
        var colors = new Dictionary<int, string> {{1, "Green"}, {2, "Blue"}, {3, "Violet"}, {4, "Orange"}};
        var containsKey = colors.ContainsKey(color);
        if (containsKey)
        {
            colorString = colors[color];
        }

        return colorString;
    }
}

Polymorphisme

Indice de maintenabilité total94
Complexité cyclomatique totale15

Interface

Indice de maintenabilité100
Complexité cyclomatique1
public interface IColor
{
    string ColorName { get; }
}

Usine

Indice de maintenabilité76
Complexité cyclomatique4
public class ColorFactory
{
    public string GetColor(int color)
    {
        IColor defaultColor = new RedColor();
        var colors = GetColors();
        var containsKey = colors.ContainsKey(color);
        if (containsKey)
        {
            var c = colors[color];
            return c.ColorName;
        }

        return defaultColor.ColorName;
    }

    private static IDictionary<int, IColor> GetColors()
    {
        return new Dictionary<int, IColor>
        {
            {1, new GreenColor()}, 
            {2, new BlueColor()}, 
            {3, new VioletColor()}, 
            {4, new OrangeColor()}, 
            {5, new MagentaColor()}
        };
    }
}

Implémentation

Indice de maintenabilité97
Complexité cyclomatique2
public class BlueColor : IColor
{
    public string ColorName => "Blue";
}

public class RedColor : IColor
{
    public string ColorName => "Red";
}

public class GreenColor : IColor
{
    public string ColorName => "Green";
}

public class MagentaColor : IColor
{
    public string ColorName => "Magenta";
}

public class VioletColor : IColor
{
    public string ColorName => "Violet";
}

Les résultats

Avant de me plonger dans les résultats, définissons la complexité cyclomatique et l’indice de maintenabilité :

  • Complexité cyclomatique est la mesure du branchement logique. Plus le nombre est bas, mieux c’est.
  • Indice de maintenabilité mesure la maintenabilité du code. Il est sur une échelle entre 0 et 100. Plus le nombre est élevé, mieux c’est.
Complexité cyclomatiqueIndice de maintenabilité
Instruction Switch672
Dictionnaire373
Polymorphisme1594

Nous examinerons d’abord la complexité cyclomatique.

Les résultats pour la complexité cyclomatique sont simples. L’implémentation du dictionnaire est la plus simple. Cela signifie-t-il que c’est la meilleure solution ? Non, comme nous le verrons lors de l’évaluation de l’indice de maintenabilité.

La plupart penseraient comme moi, l’implémentation avec la complexité cyclomatique la plus basse est la plus maintenable — comment pourrait-il en être autrement ?

Dans notre scénario, l’implémentation avec la complexité cyclomatique la plus basse n’est pas la plus maintenable. En fait, dans notre scénario, c’est l’inverse. L’implémentation la plus complexe est la plus maintenable ! L’esprit soufflé !

Si vous vous en souvenez, plus le score de l’indice de maintenabilité est élevé, mieux c’est. Pour aller droit au but, le polymorphisme a le meilleur score d’indice de maintenabilité — mais il a aussi la complexité cyclomatique la plus élevée. Qu’est-ce qui se passe ? Cela ne semble pas juste.

Pourquoi l’implémentation la plus complexe est-elle la plus maintenable ? Pour répondre à cela, nous devons comprendre l’indice de maintenabilité.

L’indice de maintenabilité se compose de 4 métriques : la complexité cyclomatique, le nombre de lignes de code, le nombre de commentaires et le volume de Halstead. Les trois premières métriques sont relativement bien connues, mais la dernière, le Volume de Halstead, est relativement inconnue. Comme la complexité cyclomatique, le Volume de Halstead tente de mesurer objectivement la complexité du code.

En termes simples, le Volume de Halstead mesure le nombre de pièces mobiles (variables, appels système, arithmétique, constructions de codage, etc.) dans le code. Plus le nombre de pièces mobiles est élevé, plus la complexité est grande. Plus le nombre de pièces mobiles est bas, plus la complexité est basse. Cela explique pourquoi l’implémentation polymorphe obtient un score élevé sur l’indice de maintenabilité ; les classes ont peu ou pas de pièces mobiles. Une autre façon de regarder le Volume de Halstead est qu’il mesure la densité des « pièces mobiles ».

Qu’est-ce que le logiciel, sinon le changement ? Pour refléter le monde réel, nous introduisons le changement. J’ai ajouté une nouvelle couleur à chaque implémentation.

Voici les résultats révisés.

Complexité cyclomatiqueIndice de maintenabilité
Instruction Switch770
Dictionnaire373
Polymorphisme1795

Les approches switch et polymorphe ont toutes deux augmenté en complexité cyclomatique d’une unité, mais intéressamment, le dictionnaire n’a pas augmenté. Au début, j’étais perplexe par cela, mais j’ai réalisé que le dictionnaire considère les couleurs comme des données et les deux autres implémentations traitent les couleurs comme du code.Je vais aller droit au but.

En tournant notre attention vers l’indice de maintenabilité, un seul, l’instruction switch, a diminué en maintenabilité. Le score de maintenabilité du polymorphisme s’est amélioré et pourtant la complexité augmente aussi (nous préférerions qu’elle diminue). Comme je l’ai mentionné ci-dessus, c’est contre-intuitif.

Notre comparaison montre que les dictionnaires peuvent, d’un point de vue de la complexité, évoluer infiniment. L’approche polymorphe est de loin la plus maintenable et semble augmenter en maintenabilité à mesure que plus de scénarios sont ajoutés. L’instruction switch augmente en complexité et diminue en maintenabilité lorsque le nouveau scénario a été ajouté. Même avant d’ajouter le nouveau scénario, elle avait les pires mesures de complexité cyclomatique et d’indice de maintenabilité.

Jem Finch de Google a partagé ses réflexions sur les lacunes des instructions switch :

1. Les implémentations de méthodes polymorphes sont lexicalement isolées les unes des autres. Les variables peuvent être ajoutées, supprimées, modifiées, etc. sans aucun risque d’impacter du code non lié dans une autre branche de l’instruction switch.

2. Les implémentations de méthodes polymorphes sont garanties de revenir au bon endroit, en supposant qu’elles se terminent. Les instructions switch dans un langage avec fall through comme C/C++/Java nécessitent une instruction « break » sujette aux erreurs pour s’assurer qu’elles reviennent à l’instruction après le switch plutôt qu’au bloc case suivant.

3. L’existence d’une implémentation de méthode polymorphe peut être appliquée par le compilateur, qui refusera de compiler le programme si une implémentation de méthode polymorphe est manquante. Les instructions switch ne fournissent pas une telle vérification d’exhaustivité.

4. La distribution de méthodes polymorphes est extensible sans accès à (ou recompilation de) autre code source. Ajouter un autre cas à une instruction switch nécessite l’accès au code de distribution d’origine, non seulement à un endroit mais à chaque endroit où l’énumération pertinente est commutée.

5. … vous pouvez tester les méthodes polymorphes indépendamment de l’appareil de commutation. La plupart des fonctions qui commutent comme l’exemple que l’auteur a donné contiendront un autre code qui ne peut alors pas être testé séparément ; les appels de méthode virtuelle, en revanche, peuvent l’être.

6. Les appels de méthode polymorphe garantissent une distribution en temps constant. Aucun compilateur suffisamment intelligent n’est nécessaire pour convertir ce qui est naturellement une construction en temps linéaire (l’instruction switch avec fall through) en une construction en temps constant.

Malheureusement, ou heureusement, selon votre camp, la plupart des langages ont une instruction switch, et ils ne vont nulle part de sitôt. Avec cela à l’esprit, il est bon de savoir ce qui se passe sous le capot lors de la compilation des instructions switch.

Il y a trois optimisations d’instructions switch qui peuvent se produire :

  1. Instructions if-elseif – Quand une instruction switch a un petit nombre de cas ou des cas clairsemés (valeurs non-incrémentales, telles que 10, 250, 1000), elle est convertie en instruction if-elseif.
  2. Table de saut – Dans les ensembles plus importants de cas adjacents (1, 2, 3, 4, 5), le compilateur convertit l’instruction switch en une table de saut. Une table de saut est essentiellement une Hashtable avec un pointeur (pensez à l’instruction goto) vers la fonction en mémoire.
  3. Recherche binaire – Pour les grands ensembles de cas clairsemés, le compilateur peut implémenter une recherche binaire pour identifier rapidement le cas, similaire à la façon dont un index fonctionne dans une base de données. Dans les cas extraordinaires où les cas sont un grand nombre de cas clairsemés et adjacents, le compilateur utilisera une combinaison des trois optimisations.

Résumé

Dans un monde orienté objet, l’instruction switch, conçue en 1952, est un pilier de l’ingénieur logiciel. Une exception notable est Smalltalk où les concepteurs ont choisi d’exclure l’instruction switch.

Comparée aux implémentations équivalentes alternatives, le dictionnaire et le polymorphisme, l’instruction switch n’a pas aussi bien réussi.

L’instruction switch est là pour rester, mais comme notre comparaison l’a montré, il existe de meilleures alternatives à l’instruction switch.

Les implémentations sont disponibles sur Github.

Auteur : Chuck Conway est un ingénieur IA avec près de 30 ans d’expérience en génie logiciel. Il construit des systèmes IA pratiques — pipelines de contenu, agents d’infrastructure et outils qui résolvent des problèmes réels — et partage ce qu’il apprend en chemin. Connectez-vous avec lui sur les réseaux sociaux : X (@chuckconway) ou visitez-le sur YouTube et sur SubStack.

↑ Retour en haut

Vous aimerez peut-être aussi