
Depuis près de 50 ans, l’instruction switch (également connue sous le nom d’instruction case) fait partie intégrante de la programmation. Ces dernières années, cependant, certains prétendent que l’instruction switch a fait son temps. 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 était dans ALGOL 58 en 1958. Plus tard, l’instruction switch a été incluse dans l’indélébile langage de programmation C, qui, comme nous le savons, a influencé la plupart des langages de programmation modernes.
Avançons rapidement jusqu’à aujourd’hui et 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/défenseurs derrière Dolphin Smalltalk a partagé ses réflexions sur pourquoi Smalltalk a exclu l’instruction switch :
Quand je suis arrivé à Smalltalk depuis C++, je ne pouvais pas comprendre comment un langage supposé complet ne supportait pas une construction switch/case. Après tout, quand j’ai d’abord évolué vers la “programmation structurée” depuis BASIC, je pensais que switch était l’une des meilleures choses depuis le pain tranché. 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 distribuent vers le 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 vers une conception OOP beaucoup plus fine que celle à laquelle j’étais habitué en C++. S’il y avait eu une instruction switch disponible, il m’aurait fallu beaucoup plus de temps pour apprendre cela ou, pire, je pourrais encore programmer dans un style pseudo-objet C++/Java en Smalltalk.
Je soutiendrais qu’en OOP normale, il n’y a pas vraiment besoin d’une instruction switch. Parfois, lors de l’interfaçage avec un monde non-OOP (comme recevoir et distribuer des messages WM_XXXX Windows qui ne sont pas des objets mais juste des entiers), alors une instruction switch serait utile. Dans ces situations, il y a des alternatives (comme distribuer depuis un Dictionary) et le nombre de fois qu’elles apparaissent ne justifie pas l’inclusion de syntaxe supplémentaire.
Andy avait-il raison ? Sommes-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 mis en place une comparaison entre une instruction switch, un dictionnaire, et le polymorphisme. Appelons cela un combat. Que la meilleure implémentation gagne !
Chaque implémentation a une méthode prenant un paramètre, un entier, et retourne 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é Cyclomatique | 6 |
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é Cyclomatique | 3 |
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é Total | 94 |
---|---|
Complexité Cyclomatique Totale | 15 |
Interface
Indice de Maintenabilité | 100 |
---|---|
Complexité Cyclomatique | 1 |
public interface IColor
{
string ColorName { get; }
}
Factory
Indice de Maintenabilité | 76 |
---|---|
Complexité Cyclomatique | 4 |
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é Cyclomatique | 2 |
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 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. C’est sur une échelle entre 0 et 100. Plus le nombre est élevé, mieux c’est.
Complexité Cyclomatique | Indice de Maintenabilité | |
---|---|---|
Instruction Switch | 6 | 72 |
Dictionnaire | 3 | 73 |
Polymorphisme | 15 | 94 |
Nous examinerons d’abord la complexité cyclomatique.
Les résultats pour la complexité cyclomatique sont directs. L’implémentation par dictionnaire est la plus simple. Cela signifie-t-il que c’est la meilleure solution ? Non, comme nous le verrons quand nous évaluerons l’indice de maintenabilité.
La plupart penseraient comme moi, l’implémentation avec la plus faible complexité cyclomatique est la plus maintenable — comment pourrait-il en être autrement ?
Dans notre scénario, l’implémentation avec la plus faible complexité cyclomatique n’est pas la plus maintenable. En fait, dans notre scénario, c’est l’opposé. L’implémentation la plus complexe est la plus maintenable ! L’esprit soufflé !
Si vous vous souvenez, plus le score d’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 plus haute complexité cyclomatique. Qu’est-ce qui se passe ? Cela ne semble pas correct.
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é consiste en 4 métriques : complexité cyclomatique, lignes de code, 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 faible, plus la complexité est faible. 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 voir le Volume de Halstead est qu’il mesure la densité des “pièces mobiles”.
Qu’est-ce que le logiciel, sinon de changer ? Pour refléter le monde réel, nous introduisons un changement. J’ai ajouté une nouvelle couleur à chaque implémentation.
Ci-dessous sont les résultats révisés.
Complexité Cyclomatique | Indice de Maintenabilité | |
---|---|---|
Instruction Switch | 7 | 70 |
Dictionnaire | 3 | 73 |
Polymorphisme | 17 | 95 |
L’instruction switch et les approches polymorphes ont toutes deux augmenté en complexité cyclomatique d’une unité, mais curieusement, le dictionnaire n’a pas augmenté. Au début j’étais perplexe par cela, mais alors 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. J’irai droit au but.
Tournant notre attention vers l’indice de maintenabilité, seulement un, 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 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é quand le nouveau scénario a été ajouté. Même avant que nous ajoutions 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 défauts 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, et ainsi de suite 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 retourner au bon endroit, en supposant qu’elles se terminent. Les instructions switch dans un langage à chute libre comme C/C++/Java nécessitent une instruction “break” sujette aux erreurs pour s’assurer qu’elles retournent à 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 imposée par le compilateur, qui refusera de compiler le programme si une implémentation de méthode polymorphe manque. 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 original, non seulement en un endroit mais dans chaque endroit où l’enum pertinent est switchée.
5. … vous pouvez tester les méthodes polymorphes indépendamment de l’appareil de commutation. La plupart des fonctions qui switchent comme l’exemple que l’auteur a donné contiendront d’autre code qui ne peut alors pas être testé séparément ; les appels de méthodes virtuelles, d’autre part, peuvent l’être.
6. Les appels de méthodes polymorphes 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 chute libre) en une construction en temps constant.
Malheureusement, ou heureusement, selon votre camp, la plupart des langages ont une instruction switch, et elles 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’instruction switch qui peuvent se produire :
- Instructions If-elseif – Quand une instruction switch a un petit nombre de cas ou des cas épars (valeurs non-incrémentales, comme 10, 250, 1000) elle est convertie en instruction if-elseif.
- Table de Saut – Dans de plus grands ensembles de cas adjacents (1, 2, 3, 4, 5) le compilateur convertit l’instruction switch en table de saut. Une Table de Saut est essentiellement une Hashtable avec un pointeur (pensez instruction goto) vers la fonction en mémoire.
- Recherche Binaire – Pour de grands ensembles de cas épars le compilateur peut implémenter une recherche binaire pour identifier le cas rapidement, similaire à comment un index fonctionne dans une base de données. Dans des cas extraordinaires où les cas sont un grand nombre de cas épars 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.
Quand comparée à des implémentations équivalentes alternatives, le dictionnaire, et le polymorphisme, l’instruction switch ne s’en est pas aussi bien sortie.
L’instruction switch est là pour rester, mais comme notre comparaison l’a montré, il y a de meilleures alternatives à l’instruction switch.
Les implémentations sont disponibles sur Github.
Auteur : Chuck Conway se spécialise dans l’ingénierie logicielle et l’IA générative. Connectez-vous avec lui sur les réseaux sociaux : X (@chuckconway) ou visitez-le sur YouTube.