J'ai toujours été d'avis que les grandes déclarations de commutateur sont le symptôme d'une mauvaise conception OOP. Dans le passé, j'avais lu des articles traitant de ce sujet et ils avaient fourni des approches alternatives OOP, généralement basées sur le polymorphisme afin d'instancier le bon objet pour gérer le cas.
Je suis maintenant dans une situation qui a une instruction switch monstre basée sur un flux de données provenant d'une socket TCP dans laquelle le protocole consiste essentiellement en une commande terminée par une nouvelle ligne, suivie de lignes de données, suivie d'un marqueur de fin. La commande peut être l'une des 100 commandes différentes, aussi j'aimerais trouver un moyen de réduire cette instruction de commutateur monstre à quelque chose de plus gérable.
J'ai fait quelques recherches sur Google pour trouver les solutions dont je me souviens, mais malheureusement, Google est devenu un terrain vague de résultats non pertinents pour de nombreux types de requêtes ces temps-ci.
Existe-t-il des modèles pour ce type de problème? Des suggestions sur des implémentations possibles?
Je pensais utiliser une recherche dans le dictionnaire, en faisant correspondre le texte de la commande au type d’objet à instancier. Cela présente l’avantage de Nice de simplement créer un nouvel objet et d’insérer une nouvelle commande/type dans la table pour toute nouvelle commande.
Cependant, cela a aussi le problème de l'explosion de type. J'ai maintenant besoin de 100 nouvelles classes et je dois également trouver un moyen de les connecter proprement au modèle de données. Le "one true switch statement" est-il vraiment la voie à suivre?
J'apprécierais vos pensées, opinions ou commentaires.
Vous pouvez tirer parti d’un Command Pattern .
Pour OOP, vous pourrez peut-être réduire plusieurs commandes similaires dans une seule classe, si les variations de comportement sont suffisamment petites pour éviter une explosion complète de la classe (ouais, j'entends déjà les gourous OOP hurler à ce sujet) . Cependant, si le système est déjà opérationnel et que chacune des 100 commandes est vraiment unique, faites-en simplement des classes uniques et tirez parti de l'héritage pour consolider les éléments communs.
Si le système n’est pas une programmation orientée objet, je n’ajouterais pas OOP uniquement à cela ... vous pouvez facilement utiliser le modèle de commande avec une simple recherche dans le dictionnaire et des pointeurs de fonction, ou même des appels de fonction générés de manière dynamique en fonction de la commande nom, en fonction de la langue. Ensuite, vous pouvez simplement grouper les fonctions associées logiquement dans des bibliothèques qui représentent une collection de commandes similaires pour obtenir une séparation gérable. Je ne sais pas s'il existe un bon terme pour ce type d'implémentation ... Je le considère toujours comme un style "répartiteur", basé sur l'approche MVC de gestion des URL.
Je vois les énoncés deux switch comme un symptôme de la conception non-OO, où le type switch-on-enum-enum pourrait être remplacé par plusieurs types offrant une implémentation différente d'une interface abstraite; par exemple, ce qui suit ...
switch (eFoo)
{
case Foo.This:
eatThis();
break;
case Foo.That:
eatThat();
break;
}
switch (eFoo)
{
case Foo.This:
drinkThis();
break;
case Foo.That:
drinkThat();
break;
}
... devrait peut-être être réécrit en tant que ...
IAbstract
{
void eat();
void drink();
}
class This : IAbstract
{
void eat() { ... }
void drink() { ... }
}
class That : IAbstract
{
void eat() { ... }
void drink() { ... }
}
Cependant, one instruction de commutateur n'est pas imo est un indicateur si puissant que l'instruction de commutateur doit être remplacée par quelque chose d'autre.
La commande peut être l'une des 100 commandes différentes
Si vous devez faire une chose sur 100, vous ne pouvez pas éviter d'avoir une branche à 100 voies. Vous pouvez l'encoder dans le flux de contrôle (switch, if-elseif ^ 100) ou dans les données (un mappage de 100 éléments d'une chaîne à une commande/fabrique/stratégie). Mais ce sera là.
Vous pouvez essayer d'isoler le résultat de la branche 100 voies des choses qui n'ont pas besoin de connaître ce résultat. Peut-être que 100 méthodes différentes suffisent; il n'est pas nécessaire d'inventer des objets inutiles si cela rend le code difficile à manier.
Je pense que c'est l'un des rares cas où les grands commutateurs sont la meilleure solution, à moins qu'une autre solution ne se présente.
Il y a deux choses qui me viennent à l’esprit lorsque l’on parle d’une grande déclaration de commutateur:
D'autre part, une implémentation de carte peut être conforme à OCP et peut fonctionner avec potentiellement O (1).
Je vois le modèle de stratégie. Si j'ai 100 stratégies différentes ... ainsi soit-il. La déclaration de commutateur géant est moche. Les noms de classe de toutes les commandes sont-ils valides? Si c'est le cas, utilisez simplement les noms de commande comme noms de classe et créez l'objet de stratégie avec Activator.CreateInstance.
Vous pouvez utiliser un dictionnaire (ou une carte de hachage si vous codez en Java) (cela s'appelle le développement dirigé par table par Steve McConnell).
Je dirais que le problème n’est pas l’énoncé majeur, mais plutôt la prolifération du code qu’il contient et l’abus de variables mal définies.
Je l'ai moi-même expérimenté dans un projet, lorsque de plus en plus de code est entré dans le commutateur, jusqu'à ce qu'il devienne impossible à maintenir. Ma solution consistait à définir sur la classe de paramètre qui contenait le contexte des commandes (nom, paramètres, collectés avant le commutateur), créer une méthode pour chaque instruction case et appeler cette méthode avec l'objet paramètre de la casse.
Bien sûr, un répartiteur de commande entièrement OOP (basé sur une magie telle que la réflexion ou des mécanismes tels que l'activation Java) est plus beau, mais parfois vous voulez juste réparer les choses et faire le travail;)
Oui, je pense que les grandes déclarations de cas sont un symptôme indiquant que son code peut être amélioré… généralement en mettant en œuvre une approche plus orientée objet. Par exemple, si je me trouve à évaluer le type de classes dans une instruction switch, cela signifie presque toujours que je pourrais probablement utiliser Generics pour éliminer l'instruction switch.
Je vois un moyen d’améliorer votre code en fonction des données. Ainsi, pour chaque code, vous correspondez à quelque chose qui le gère (fonction, objet). Vous pouvez également utiliser la réflexion pour mapper des chaînes représentant les objets/fonctions et les résoudre au moment de l'exécution, mais vous souhaiterez peut-être effectuer quelques expériences pour évaluer les performances.
Pensez à la façon dont Windows a été écrit à l'origine dans la pompe de message d'application. C'est nul. Les applications s'exécuteraient plus lentement avec plus d'options de menu que vous avez ajoutées. Au fur et à mesure que la commande recherchée finissait de plus en plus loin vers le bas de la déclaration de commutateur, l'attente de la réponse était de plus en plus longue. Il n'est pas acceptable d'avoir de longues déclarations de commutateur, point final. J'ai créé un démon AIX en tant que gestionnaire de commandes POS capable de gérer 256 commandes uniques sans même savoir ce qu'il y avait dans le flux de demandes reçu via TCP/IP. Le tout premier caractère du flux était un index dans un tableau de fonctions. Tout index non utilisé était défini sur un gestionnaire de messages par défaut; connectez-vous et dites au revoir.
Vous pouvez également adopter une approche linguistique et définir les commandes avec les données associées dans une grammaire. Vous pouvez ensuite utiliser un outil générateur pour analyser la langue. J'ai utilisé Irony à cette fin. Sinon, vous pouvez utiliser le modèle d'interprétation.
À mon avis, l'objectif n'est pas de construire le modèle le plus pur OO, mais de créer un système flexible, extensible, maintenable et puissant.
J'ai récemment eu un problème similaire avec une énorme instruction switch et je me suis débarrassé du commutateur laid par la solution la plus simple {simple} _ et une {table de consultation} _ ainsi qu'une fonction ou une méthode renvoyant la valeur attendue. le modèle de commande est une bonne solution, mais avoir 100 classes n’est pas une solution, je pense .. alors j’avais quelque chose comme:
switch(id)
case 1: DoSomething(url_1) break;
case 2: DoSomething(url_2) break;
..
..
case 100 DoSomething(url_100) break;
et j'ai changé pour:
string url = GetUrl(id);
DoSomthing(url);
getUrl peut aller dans la base de données et renvoyer l'URL que vous recherchez, ou pourrait être un dictionnaire en mémoire contenant les 100 urls . J'espère que cela pourrait aider n'importe qui à remplacer des énormes instructions switch.
La meilleure façon de gérer ce problème particulier: la sérialisation et les protocoles correctement consiste à utiliser un IDL et à générer le code de marshaling avec des instructions switch. Parce que quels que soient les modèles (prototype, modèle de commande, etc.) que vous essayez d’utiliser autrement, vous devrez initialiser un mappage entre un ID de commande/chaîne et un pointeur de classe/fonction. Le compilateur peut utiliser une recherche de hachage parfaite pour les instructions switch.