Carte des fonctions vs instruction switch


21

Je travaille sur un projet qui traite les demandes, et il y a deux composants à la demande: la commande et les paramètres. Le gestionnaire de chaque commande est très simple (<10 lignes, souvent <5). Il existe au moins 20 commandes et en aura probablement plus de 50.

J'ai trouvé quelques solutions:

  • un gros interrupteur / commandes if-else on
  • carte des commandes aux fonctions
  • mappage des commandes aux classes / singletons statiques

Chaque commande effectue une petite vérification d'erreur, et le seul bit qui peut être extrait est la vérification du nombre de paramètres, qui est défini pour chaque commande.

Quelle serait la meilleure solution à ce problème et pourquoi? Je suis également ouvert à tous les modèles de conception que j'ai pu manquer.

J'ai trouvé la liste des avantages et inconvénients suivants pour chacun:

commutateur

  • avantages
    • conserve toutes les commandes dans une seule fonction; car ils sont simples, cela en fait une table de recherche visuelle
    • pas besoin d'encombrer la source avec des tonnes de petites fonctions / classes qui ne seront utilisées qu'en un seul endroit
  • les inconvénients
    • très long
    • difficile d'ajouter des commandes par programme (besoin de chaîner en utilisant la casse par défaut)

commandes de carte -> fonction

  • avantages
    • petits morceaux de la taille d'une bouchée
    • peut ajouter / supprimer des commandes par programme
  • les inconvénients
    • si fait en ligne, même visuellement que le commutateur
    • si ce n'est pas fait en ligne, beaucoup de fonctions ne sont utilisées qu'au même endroit

commandes de carte -> classe statique / singleton

  • avantages
    • peut utiliser le polymorphisme pour gérer la vérification d'erreur simple (seulement comme 3 lignes, mais quand même)
    • avantages similaires à la carte -> solution fonctionnelle
  • les inconvénients
    • beaucoup de très petites classes encombreront le projet
    • implémentation pas tous au même endroit, il n'est donc pas aussi facile d'analyser les implémentations

Notes supplémentaires:

J'écris ceci dans Go, mais je ne pense pas que la solution soit spécifique à la langue. Je cherche une solution plus générale car je devrai peut-être faire quelque chose de très similaire dans d'autres langues.

Une commande est une chaîne, mais je peux facilement la mapper à un nombre si cela est pratique. La signature de fonction est quelque chose comme:

Reply Command(List<String> params)

Go a des fonctions de haut niveau, et d'autres plates-formes que j'envisage ont également des fonctions de haut niveau, d'où la différence entre les deuxième et troisième options.


8
mappez les commandes aux fonctions et chargez-les lors de l'exécution à partir de la configuration. utilisez le modèle de commande.
Steven Evers

3
N'ayez pas trop peur de créer plein de petites fonctions. Habituellement, une collection de plusieurs petites fonctions est plus facile à gérer qu'une énorme fonction.
Bart van Ingen Schenau

8
Qu'y a-t-il dans ce monde à l'envers, où les gens répondent aux questions dans les commentaires et demandent plus d'informations dans les réponses?
pdr

4
@SteveEvers: S'il n'a pas besoin d'être élaboré, c'est une réponse, aussi courte soit-elle. Si c'est le cas, et que vous n'avez pas le temps ou quoi que ce soit, laissez-le à quelqu'un d'autre de répondre (c'est toujours comme tricher pour écrire une réponse qui confirme un commentaire qui a déjà une demi-douzaine de votes positifs). Personnellement, je pense que cela nécessite une élaboration, le PO veut vraiment savoir pourquoi la meilleure solution est la meilleure solution.
pdr

1
@pdr - C'est vrai. Mon inclination était une carte des commandes aux fonctions, mais je suis un programmeur relativement junior dans un cours de conception CS. Mon professeur aime beaucoup de cours, il y a donc au moins 2 solutions légitimes. Je voulais connaître le favori de la communauté.
beatgammit

Réponses:


14

C'est un bon choix pour une carte (2e ou 3e solution proposée). Je l'ai utilisé des dizaines de fois, et c'est simple et efficace. Je ne fais pas vraiment de distinction entre ces solutions; le point important est qu'il existe une carte avec des noms de fonction comme clés.

Le principal avantage de l'approche cartographique, à mon avis, est que le tableau est constitué de données. Cela signifie qu'il peut être contourné, augmenté ou autrement modifié au moment de l'exécution; il est également facile de coder des fonctions supplémentaires qui interprètent la carte de manière nouvelle et passionnante. Cela ne serait pas possible avec la solution boîtier / commutateur.

Je n'ai pas vraiment connu les inconvénients que vous mentionnez, mais je voudrais mentionner un inconvénient supplémentaire: la répartition est facile si seul le nom de la chaîne est important, mais si vous devez prendre en compte des informations supplémentaires afin de décider quelle fonction exécuter , c'est beaucoup moins propre.

Peut-être que je n'ai jamais rencontré de problème assez difficile, mais je vois peu de valeur à la fois dans le modèle de commande et dans l'encodage de la répartition en tant que hiérarchie de classe, comme d'autres l'ont mentionné. Le cœur du problème est de mapper les demandes aux fonctions; une carte est simple, évidente et facile à tester. Une hiérarchie de classes nécessite plus de code et de conception, augmente le couplage entre ce code et peut vous obliger à prendre plus de décisions à l'avance que vous devrez plus tard modifier. Je ne pense pas que le modèle de commande s'applique du tout.


4

Votre problème se prête très bien au modèle de conception Command . Donc, fondamentalement, vous aurez une Commandinterface de base , puis il y aurait plusieurs CommandImplclasses qui implémenteraient cette interface. L'interface doit essentiellement avoir une seule méthode doCommand(Args args). Vous pouvez faire passer les arguments via une instance de Argsclasse. De cette façon, vous exploitez la puissance du polymorphisme au lieu d'instructions if / else maladroites. De plus, cette conception est facilement extensible.


3

Chaque fois que je me demande si je dois utiliser une instruction switch ou un polymorphisme de style OO, je me réfère au problème d'expression . Fondamentalement, si vous avez différents "cas" pour vos données et baguette pour prendre en charge différentes "actions" (où chaque action fait quelque chose de différent pour chaque cas), il est très difficile de créer un système qui vous permet naturellement d'ajouter à la fois de nouveaux cas et de nouvelles actions A l'avenir.

Si vous utilisez des instructions switch (ou le modèle Visitor), il est facile d'ajouter de nouvelles actions (parce que vous écrivez tout dans une seule fonction) mais difficile d'ajouter de nouveaux cas (car vous devez revenir en arrière et modifier les anciennes fonctions)

Inversement, si vous utilisez le polymorphisme de style OO, il est facile d'ajouter de nouveaux cas (juste créer une nouvelle classe) mais difficile d'ajouter des méthodes à une interface (car alors vous devez revenir en arrière et éditer un tas de classes)

Dans votre cas, vous n'avez qu'une seule méthode que vous devez prendre en charge (traiter une demande) mais beaucoup de cas possibles (chaque commande différente). Comme il est plus important d'ajouter de nouveaux cas plus facilement que d'ajouter de nouvelles méthodes, créez simplement une classe distincte pour chaque commande différente.


Soit dit en passant, du point de vue de l'extensibilité des choses, cela ne fait pas une grande différence si vous utilisez des classes ou des fonctions. Si nous comparons une instruction switch, l'important est de savoir comment les choses sont «réparties» et les classes et les fonctions sont réparties de la même manière. Par conséquent, utilisez simplement ce qui est plus pratique dans votre langue (et comme Go a une portée et des fermetures lexicales, la distinction entre les classes et les fonctions est en fait très petite).

Par exemple, vous pouvez généralement utiliser la délégation pour faire la partie de vérification des erreurs au lieu de compter sur l'héritage (mon exemple est en Javascript parce que je ne connais pas la syntaxe Go, j'espère que cela ne vous dérange pas)

function make_command(real_command){
    return function(x){
        if(check_arguments(x)){
            return real_command(x);
        }else{
            //handle error here
        }
    }
 }

 command1 = make_command(function(x){ 
     //do something
 })

 command2 = make_command(function(x){
     //do something else
 })

 command1(17);
 commnad2(42);

Bien sûr, cet exemple suppose qu'il existe un moyen judicieux pour la fonction wrapper ou la classe parent de vérifier les arguments pour chaque cas. Selon la façon dont les choses se passent, il peut être plus simple de placer l'appel à check_arguments dans les commandes elles-mêmes (car chaque commande peut avoir besoin d'appeler la fonction de vérification avec des arguments différents, en raison d'un nombre différent d'arguments, de types de commandes différents, etc.)

tl; dr: Il n'y a pas de meilleur moyen de résoudre tous les problèmes. Du point de vue de «faire fonctionner les choses», concentrez-vous sur la création de vos abstractions de manière à appliquer des invariants importants et à vous protéger contre les erreurs. Du point de vue de la "pérennité", gardez à l'esprit quelles parties du code sont plus susceptibles d'être étendues.


1

Je n'ai jamais utilisé go, en tant que programmeur ac #, je descendrais probablement la ligne suivante, j'espère que cette architecture s'adaptera à ce que vous faites.

Je créerais une petite classe / objet pour chacun avec la fonction principale à exécuter, chacun devrait connaître sa représentation sous forme de chaîne. Cela vous donne une connectivité qui vous semble être souhaitée, car le nombre de fonctions augmentera. Notez que je n'utiliserais pas de statique sauf si vous en avez vraiment besoin, ils ne donnent pas beaucoup d'avantages.

J'aurais alors une usine qui sait découvrir ces classes au moment de l'exécution en les modifiant pour être chargées à partir de différents assemblages, etc. cela signifie que vous n'avez pas besoin de toutes dans le même projet.

Cela le rend également plus modulaire pour les tests et devrait rendre le code agréable et petit, ce qui est plus facile à maintenir plus tard.


0

Comment sélectionnez-vous vos commandes / fonctions?

S'il existe un moyen "intelligent" de sélectionner la fonction correcte, c'est la voie à suivre - cela signifie que vous pouvez ajouter un nouveau support (peut-être dans une bibliothèque externe) sans modifier la logique principale.

En outre, le test de fonctions individuelles est plus facile en vase clos qu'une énorme instruction switch.

Enfin, n'étant utilisé qu'à un seul endroit - vous pourriez constater qu'une fois que vous aurez atteint 50, différents bits de différentes fonctions pourront être réutilisés?


Les commandes sont des chaînes uniques. Je peux les associer à des entiers si nécessaire.
beatgammit

0

Je n'ai aucune idée du fonctionnement de Go, mais une architecture que j'ai utilisée dans ActionScript est d'avoir une liste à double lien qui agit comme une chaîne de responsabilité. Chaque lien a une fonction determineResponsibility (que j'ai implémentée comme rappel, mais vous pouvez écrire chaque lien séparément si cela fonctionne mieux pour ce que vous essayez de faire). Si un lien déterminait qu'il avait la responsabilité, il appellerait meetResponsibility (encore une fois, un rappel), et cela mettrait fin à la chaîne. S'il n'était pas responsable, il transmettrait la demande au maillon suivant de la chaîne.

Différents cas d'utilisation pourraient être facilement ajoutés et supprimés simplement en ajoutant un nouveau lien entre les liens de (ou à la fin de) la chaîne existante.

Ceci est similaire, mais subtilement différent de, votre idée d'une carte des fonctions. La bonne chose est que vous passez simplement la demande et elle fait son travail sans que vous ayez à faire autre chose. L'inconvénient est que cela ne fonctionne pas bien si vous devez retourner une valeur.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.