Quels sont les moyens d'éliminer l'utilisation de switch dans le code?
Les instructions de commutateur ne sont pas un antipattern en soi, mais si vous codez en fonction d'un objet, vous devez déterminer si l'utilisation d'un commutateur est mieux résolue avec polymorphisme au lieu d'utiliser une instruction de commutateur.
Avec polymorphisme, ceci:
foreach (var animal in Zoo) {
switch (typeof(animal)) {
case "dog":
echo animal.bark();
break;
case "cat":
echo animal.meow();
break;
}
}
devient ceci:
foreach (var animal in Zoo) {
echo animal.speak();
}
Voir le odeur des instructions :
Généralement, des instructions de commutateur similaires sont dispersées dans un programme. Si vous ajoutez ou supprimez une clause dans un commutateur, vous devez souvent rechercher et réparer les autres.
Les méthodes Refactoring et Refactoring to Patterns ont des approches pour résoudre ce problème.
Si votre (pseudo) code ressemble à:
class RequestHandler {
public void handleRequest(int action) {
switch(action) {
case LOGIN:
doLogin();
break;
case LOGOUT:
doLogout();
break;
case QUERY:
doQuery();
break;
}
}
}
Ce code enfreint le Principe d'Open Closed et est fragile à tout nouveau type de code d'action qui apparaît. Pour remédier à cela, vous pouvez introduire un objet 'Commande':
interface Command {
public void execute();
}
class LoginCommand implements Command {
public void execute() {
// do what doLogin() used to do
}
}
class RequestHandler {
private Map<Integer, Command> commandMap; // injected in, or obtained from a factory
public void handleRequest(int action) {
Command command = commandMap.get(action);
command.execute();
}
}
Si votre (pseudo) code ressemble à:
class House {
private int state;
public void enter() {
switch (state) {
case INSIDE:
throw new Exception("Cannot enter. Already inside");
case OUTSIDE:
state = INSIDE;
...
break;
}
}
public void exit() {
switch (state) {
case INSIDE:
state = OUTSIDE;
...
break;
case OUTSIDE:
throw new Exception("Cannot leave. Already outside");
}
}
Ensuite, vous pouvez introduire un objet 'State'.
// Throw exceptions unless the behavior is overriden by subclasses
abstract class HouseState {
public HouseState enter() {
throw new Exception("Cannot enter");
}
public HouseState leave() {
throw new Exception("Cannot leave");
}
}
class Inside extends HouseState {
public HouseState leave() {
return new Outside();
}
}
class Outside extends HouseState {
public HouseState enter() {
return new Inside();
}
}
class House {
private HouseState state;
public void enter() {
this.state = this.state.enter();
}
public void leave() {
this.state = this.state.leave();
}
}
J'espère que cela t'aides.
Un commutateur est un modèle, qu’il soit implémenté avec une instruction switch, si autre chaîne, une table de recherche, un polymorphisme oop, une correspondance de modèle ou autre chose.
Voulez-vous éliminer l'utilisation de la " instruction de commutateur " ou du modèle de commutateur ( "? Le premier peut être éliminé, le second, uniquement si un autre modèle/algorithme peut être utilisé, et la plupart du temps, ce n'est pas possible ou ce n'est pas une meilleure approche pour le faire.
Si vous souhaitez éliminer l'instruction de l'instruction switch , la première question à poser est où est-il judicieux d'éliminer l'instruction switch et utilisez une autre technique. Malheureusement, la réponse à cette question est spécifique à un domaine.
Et rappelez-vous que les compilateurs peuvent effectuer diverses optimisations pour changer d'instruction. Ainsi, par exemple, si vous souhaitez traiter efficacement les messages, une instruction switch est la solution. Cependant, l’exécution de règles d’entreprise basées sur une instruction switch n’est probablement pas la meilleure solution et l’application doit être ré-archivée.
Voici quelques alternatives pour changer d'instruction:
Changer en soi n'est pas si mal que ça, mais si vous avez beaucoup de "switch" ou de "if/else" sur les objets de vos méthodes, cela peut être un signe que votre conception est un peu "procédurale" et que vos objets ne sont que de la valeur seaux. Déplacez la logique vers vos objets, appelez une méthode sur vos objets et laissez-les décider comment répondre à la place.
Je pense que le meilleur moyen est d'utiliser une bonne carte. À l'aide d'un dictionnaire, vous pouvez mapper presque n'importe quelle entrée avec une autre valeur/objet/fonction.
votre code ressemblerait à quelque chose (psuedo) comme ceci:
void InitMap(){
Map[key1] = Object/Action;
Map[key2] = Object/Action;
}
Object/Action DoStuff(Object key){
return Map[key];
}
Tout le monde aime l'énorme if else
blocs. Si facile à lire! Je suis toutefois curieux de savoir pourquoi vous voudriez supprimer les déclarations de substitution. Si vous avez besoin d'une instruction switch, vous avez probablement besoin d'une instruction switch. Sérieusement, je dirais que cela dépend de ce que le code fait. Si tout ce que le commutateur fait est d’appeler des fonctions (par exemple), vous pouvez passer des pointeurs de fonction. Que ce soit une solution meilleure est discutable.
La langue est un facteur important ici aussi, je pense.
Je pense que ce que vous recherchez, c'est le modèle de stratégie.
Cela peut être mis en œuvre de différentes manières, mentionnées dans d'autres réponses à cette question, telles que:
Les instructions switch
seraient bonnes à remplacer si vous ajoutiez de nouveaux états ou un nouveau comportement aux instructions:
int state; String getString () { switch (état) { cas 0: // comportement pour l'état 0 retourne "zéro"; cas 1: // comportement pour l'état 1 retourne "un"; } lance un nouvel objet IllegalStateException (); } double getDouble () { switch (this.state) { cas 0: // comportement pour l'état 0 renvoyer 0d; cas 1: // comportement pour l'état 1 renvoyer 1d; } lancer une nouvelle exception IllegalStateException (); }
Ajouter un nouveau comportement nécessite la copie de switch
, et ajouter de nouveaux états signifie ajouter un autre case
à chaque switch
déclaration.
En Java, vous ne pouvez changer qu'un nombre très limité de types primitifs dont vous connaissez les valeurs au moment de l'exécution. Cela pose un problème en soi: les états sont représentés par des nombres ou des caractères magiques.
Pattern matching , et plusieurs if - else
_ blocs peuvent être utilisés, même si vous rencontrez les mêmes problèmes lorsque vous ajoutez de nouveaux comportements et de nouveaux états.
La solution que d'autres ont suggérée comme "polymorphisme" est une instance de modèle d'état :
Remplacez chacun des états par sa propre classe. Chaque comportement a sa propre méthode sur la classe:
État final; Chaîne getString () { Return state.getString (); } double getDouble () { renvoie l'état.getDouble (); }
Chaque fois que vous ajoutez un nouvel état, vous devez ajouter une nouvelle implémentation de l'interface IState
. Dans un monde switch
, vous ajouteriez un case
à chaque switch
.
Chaque fois que vous ajoutez un nouveau comportement, vous devez ajouter une nouvelle méthode à l'interface IState
et à chacune des implémentations. C'est la même charge qu'avant, bien que maintenant le compilateur vérifiera que vous avez des implémentations du nouveau comportement sur chaque état préexistant.
D'autres ont déjà dit que c'était peut-être un poids trop lourd, alors bien sûr, il y a un point où vous atteignez l'endroit où vous vous déplacez. Personnellement, la deuxième fois que j'écris un commutateur, c'est le moment où je refacture.
sinon
Je réfute l'hypothèse selon laquelle le commutateur est intrinsèquement mauvais.
Eh bien, pour ma part, je ne savais pas que l’interrupteur était un anti-modèle.
Deuxièmement, switch peut toujours être remplacé par des instructions if/else if.
Pourquoi voulez-vous? Dans les mains d'un bon compilateur, une instruction switch peut être beaucoup plus efficace que si les blocs if/else (en plus d'être plus facile à lire), et seuls les plus gros commutateurs sont susceptibles d'être accélérés s'ils sont remplacés par une sorte de la structure de données de recherche indirecte.
'switch' est juste une construction de langage et toutes les constructions de langage peuvent être considérées comme des outils permettant d'accomplir un travail. Comme avec de vrais outils, certains outils sont mieux adaptés à une tâche qu’une autre (vous n’utiliseriez pas de marteau pour former un crochet pour l’image). La partie importante est de savoir comment "faire le travail" est défini. Doit-il être maintenable, doit-il être rapide, doit-il être adapté, doit-il être extensible, etc.?.
À chaque étape du processus de programmation, il existe généralement une gamme de constructions et de modèles pouvant être utilisés: un commutateur, une séquence if-else-si, des fonctions virtuelles, des tables de sauts, des cartes avec des pointeurs de fonction, etc. Avec l'expérience, un programmeur saura instinctivement le bon outil à utiliser pour une situation donnée.
Il faut supposer que le maintien ou la révision du code est au moins aussi compétent que l'auteur d'origine, de sorte que toute construction peut être utilisée en toute sécurité.
Dans un langage procédural, tel que C, basculer sera alors préférable à toutes les alternatives.
Dans un langage orienté objet, il existe presque toujours d'autres alternatives permettant de mieux utiliser la structure de l'objet, notamment le polymorphisme.
Le problème posé par les instructions de commutateur se produit lorsque plusieurs blocs de commutateur très similaires se produisent à plusieurs endroits de l'application et que la prise en charge d'une nouvelle valeur doit être ajoutée. Il est assez courant qu'un développeur oublie d'ajouter la prise en charge de la nouvelle valeur à l'un des blocs de commutateur dispersés dans l'application.
Avec le polymorphisme, une nouvelle classe remplace la nouvelle valeur et le nouveau comportement est ajouté lors de l'ajout de la nouvelle classe. Le comportement à ces points de commutation est ensuite hérité de la super-classe, remplacé pour fournir un nouveau comportement ou implémenté pour éviter une erreur du compilateur lorsque la super méthode est abstraite.
Lorsqu'il n'y a pas de polymorphisme évident, il peut être intéressant de mettre en œuvre le modèle de stratégie .
Mais si votre alternative est un gros SI ... ALORS ... SAUF bloc, alors oubliez ça.
Utilisez un langage qui ne vient pas avec une instruction switch intégrée. Perl 5 me vient à l’esprit.
Sérieusement, pourquoi voudriez-vous l'éviter? Et si vous avez de bonnes raisons de l'éviter, pourquoi ne pas simplement l'éviter alors?
Pour C++
Si vous vous référez, par exemple, à un AbstractFactory, je pense qu’une méthode registerCreatorFunc (..) est généralement préférable à la nécessité d’ajouter un cas pour chaque "nouvelle" instruction nécessaire. Ensuite, en laissant toutes les classes créer et enregistrer un creatorFunction (..) qui peut être facilement implémenté avec une macro (si j'ose le dire). Je crois que c'est une approche commune à de nombreux cadres. Je l'ai d'abord vu dans ET ++ et je pense que de nombreux frameworks nécessitant une macro DECL et IMPL l'utilisent.
Les pointeurs de fonctions sont un moyen de remplacer une énorme instruction de commutation volumineuse. Ils conviennent particulièrement aux langues où vous pouvez capturer des fonctions par leur nom et créer des éléments avec elles.
Bien sûr, vous ne devez pas forcer les instructions de changement de code, et il y a toujours une chance que vous le fassiez complètement, ce qui entraîne des morceaux de code redondants et stupides. (Ceci est parfois inévitable, mais une bonne langue devrait vous permettre de supprimer la redondance tout en restant propre.)
C'est un très bon exemple de division et de conquête:
Supposons que vous ayez un interprète.
switch(*IP) {
case OPCODE_ADD:
...
break;
case OPCODE_NOT_ZERO:
...
break;
case OPCODE_JUMP:
...
break;
default:
fixme(*IP);
}
Au lieu de cela, vous pouvez utiliser ceci:
opcode_table[*IP](*IP, vm);
... // in somewhere else:
void opcode_add(byte_opcode op, Vm* vm) { ... };
void opcode_not_zero(byte_opcode op, Vm* vm) { ... };
void opcode_jump(byte_opcode op, Vm* vm) { ... };
void opcode_default(byte_opcode op, Vm* vm) { /* fixme */ };
OpcodeFuncPtr opcode_table[256] = {
...
opcode_add,
opcode_not_zero,
opcode_jump,
opcode_default,
opcode_default,
... // etc.
};
Notez que je ne sais pas comment supprimer la redondance de opcode_table en C. Je devrais peut-être poser une question à ce sujet. :)
Si le commutateur est là pour distinguer différents types d'objets, il vous manque probablement des classes pour décrire ces objets avec précision, ou des méthodes virtuelles ...
Cela dépend pourquoi vous voulez le remplacer!
De nombreux interprètes utilisent 'gotos calculés' au lieu d'instructions switch pour l'exécution de l'opcode.
Ce qui me manque à propos du commutateur C/C++, c'est le Pascal 'in' et les gammes. J'aimerais aussi pouvoir allumer des cordes. Mais ceux-ci, bien que triviaux pour un compilateur, sont un travail difficile quand ils utilisent des structures et des itérateurs. Donc, au contraire, il y a beaucoup de choses que je souhaiterais pouvoir remplacer par un commutateur, si seulement le commutateur de C () était plus flexible!
En JavaScript avec tableau associatif:
this:
function getItemPricing(customer, item) {
switch (customer.type) {
// VIPs are awesome. Give them 50% off.
case 'VIP':
return item.price * item.quantity * 0.50;
// Preferred customers are no VIPs, but they still get 25% off.
case 'Preferred':
return item.price * item.quantity * 0.75;
// No discount for other customers.
case 'Regular':
case
default:
return item.price * item.quantity;
}
}
devient ceci:
function getItemPricing(customer, item) {
var pricing = {
'VIP': function(item) {
return item.price * item.quantity * 0.50;
},
'Preferred': function(item) {
if (item.price <= 100.0)
return item.price * item.quantity * 0.75;
// Else
return item.price * item.quantity;
},
'Regular': function(item) {
return item.price * item.quantity;
}
};
if (pricing[customer.type])
return pricing[customer.type](item);
else
return pricing.Regular(item);
}
Changer n'est pas une bonne solution car il casse l'Open Close Principal. C'est comme ça que je le fais.
public class Animal
{
public abstract void Speak();
}
public class Dog : Animal
{
public virtual void Speak()
{
Console.WriteLine("Hao Hao");
}
}
public class Cat : Animal
{
public virtual void Speak()
{
Console.WriteLine("Meauuuu");
}
}
Et voici comment l'utiliser (en prenant votre code):
foreach (var animal in Zoo)
{
echo animal.speak();
}
Fondamentalement, nous déléguons la responsabilité à la classe des enfants au lieu de laisser le parent décider quoi faire avec les enfants.
Vous voudrez peut-être aussi lire le "Principe de substitution de Liskov".
La réponse la plus évidente, indépendante du langage, consiste à utiliser une série de "si".
Si la langue que vous utilisez a des pointeurs de fonction (C) ou des fonctions qui sont des valeurs de 1ère classe (Lua), vous pouvez obtenir des résultats similaires à un "commutateur" en utilisant un tableau (ou une liste) de fonctions (pointeurs sur).
Vous devriez être plus précis sur la langue si vous voulez de meilleures réponses.
Les instructions de commutateur peuvent souvent être remplacées par une bonne conception OO.
Par exemple, vous avez une classe Account et utilisez une instruction switch pour effectuer un calcul différent en fonction du type de compte.
Je suggérerais que cela devrait être remplacé par un certain nombre de classes de comptes, représentant les différents types de comptes, et toutes implémentant une interface de compte.
Le changement devient alors inutile car vous pouvez traiter tous les types de comptes de la même manière et, grâce au polymorphisme, le calcul approprié sera exécuté pour le type de compte.