Comment refactoriser une application avec plusieurs boîtiers de commutation?

10

J'ai une application qui prend un entier en entrée et basée sur les appels d'entrée des méthodes statiques de différentes classes. Chaque fois qu'un nouveau numéro est ajouté, nous devons ajouter un autre cas et appeler une méthode statique différente d'une classe différente. Il y a maintenant 50 cas dans le commutateur et chaque fois que j'ai besoin d'ajouter un autre cas, je frémis. Y a-t-il une meilleure manière de faire cela.

J'ai réfléchi et j'ai eu cette idée. J'utilise le modèle de stratégie. Au lieu d'avoir un boîtier de commutation, j'ai une carte des objets de stratégie avec la clé étant l'entier d'entrée. Une fois la méthode invoquée, elle recherchera l'objet et appellera la méthode générique de l'objet. De cette façon, je peux éviter d'utiliser la construction de boîtier de commutation.

Qu'est-ce que tu penses?

Kaushik Chakraborty
la source
2
Quel est le problème réel avec le code actuel?
Philip Kendall
Que se passe-t-il lorsque vous devez effectuer l'un de ces changements? Devez-vous ajouter un switchcas et appeler une méthode préexistante dans votre système complexe, ou devez-vous inventer à la fois la méthode et son appel?
Kilian Foth
@KilianFoth J'ai hérité de ce projet en tant que développeur de maintenance et je n'ai pas encore eu à apporter de modifications. Je vais cependant apporter des modifications bientôt, je veux donc refactoriser maintenant. Mais pour répondre à votre question, oui à cette dernière.
Kaushik Chakraborty
2
Je pense que vous devez montrer un exemple condensé de ce qui se passe.
whatsisname
1
@KaushikChakraborty: composez ensuite un exemple à partir de la mémoire. Il y a des situations où un uber-switch de 250+ cas est approprié, et il y a des cas où le switch est mauvais, peu importe le nombre de cas. Le diable est dans les détails et nous n'avons aucun détail.
whatsisname

Réponses:

13

Il y a maintenant 50 cas dans le commutateur et chaque fois que j'ai besoin d'ajouter un autre cas, je frémis.

J'adore le polymorphisme. J'adore SOLID. J'adore la programmation orientée objet pure. Je déteste voir ceux-ci donnés une mauvaise réputation parce qu'ils sont appliqués de manière dogmatique.

Vous n'avez pas fait de bons arguments en faveur d'une refactorisation de la stratégie. Le refactoring a d'ailleurs un nom. Cela s'appelle Remplacer le conditionnel par le polymorphisme .

J'ai trouvé quelques conseils pertinents pour vous sur c2.com :

Cela n'a de sens que si les tests conditionnels identiques ou très similaires sont répétés souvent. Pour des tests simples et rarement répétés, le remplacement d'un conditionnel simple par la verbosité de plusieurs définitions de classe, et le déplacement probable de tout cela loin du code qui nécessite réellement l'activité conditionnellement requise, résulterait en un exemple classique d'obscurcissement de code. Préférez la clarté à la pureté dogmatique. - DanMuller

Vous avez un interrupteur avec 50 cas et votre alternative est de produire 50 objets. Oh et 50 lignes de code de construction d'objet. Ce n'est pas un progrès. Pourquoi pas? Parce que ce refactoring ne fait rien pour réduire le nombre de 50. Vous utilisez ce refactoring lorsque vous trouvez que vous devez créer une autre instruction switch sur la même entrée ailleurs. C'est à ce moment que cette refactorisation est utile car elle transforme 100 en 50.

Tant que vous faites référence à "l'interrupteur" comme si c'était le seul que vous ayez, je ne le recommande pas. Le seul avantage de la refactorisation maintenant est qu'elle réduit les chances que certains goofball copient et collent votre commutateur 50 cases.

Ce que je recommande, c'est d'examiner attentivement ces 50 cas afin de trouver des points communs qui peuvent être pris en compte. Je veux dire 50? Vraiment? Vous êtes sûr d'avoir besoin de tant de cas? Vous essayez peut-être de faire trop ici.

candied_orange
la source
Je suis d'accord avec ce que vous dites. Le code a beaucoup de redondances, il se pourrait que beaucoup de cas ne soient même pas nécessaires, mais à première vue, cela ne semble pas être le cas. Chacun des cas appelle une méthode qui appelle plusieurs systèmes et agrège les résultats et retourne au code appelant. Chaque classe est autonome, fait un travail et j'ai peur de violer le principe de haute cohésion, si je réduisais le nombre de cas.
Kaushik Chakraborty
2
Je peux en obtenir 50 sans violer une cohésion élevée et garder les choses autonomes. Je ne peux pas le faire avec un seul numéro. J'aurais besoin d'un 2, d'un 5 et d'un autre 5. C'est pourquoi cela s'appelle l'affacturage. Sérieusement, regardez toute votre architecture et voyez si vous ne pouvez pas trouver un moyen de sortir de cet enfer de 50 cas dans lequel vous êtes. La refactorisation consiste à annuler de mauvaises décisions. Ne pas les perpétuer sous de nouvelles formes.
candied_orange
Maintenant, si vous pouvez voir un moyen de réduire les 50 en utilisant ce refactoring, allez-y. Pour tirer parti de l'idée de Doc Browns: Une carte de cartes peut prendre deux clés. Quelque chose à quoi penser.
candied_orange
1
Je suis d'accord avec le commentaire de Candied. Le problème n'est pas les 50 cas dans l'instruction switch, le problème est la conception architecturale de niveau supérieur qui vous oblige à appeler une fonction qui doit décider entre 50 options. J'ai conçu des systèmes très grands et complexes et je n'ai jamais été contraint à une telle situation.
Dunk
@Candied "Vous utilisez ce refactoring lorsque vous trouvez que vous avez besoin de créer une autre instruction switch sur la même entrée ailleurs." Pouvez-vous élaborer ceci? projet première autorisation, validation, procédures CRUD puis dao. Donc, dans chaque couche, il y a les cas de commutation sur la même entrée, c'est-à-dire un entier, mais faisant différentes fonctions comme auth, valides. devrions-nous donc créer une classe de sapin de chaque type qui a des méthodes différentes? Notre cas correspond-il à ce que vous essayez de dire en "répétant le même interrupteur sur la même entrée"?
Siddharth Trikha
9

Une carte des objets de stratégie seuls, qui est initialisée dans une fonction de votre code, où vous avez plusieurs lignes de code ressemblant à

     myMap.Add(1,new Strategy1());
     myMap.Add(2,new Strategy2());
     myMap.Add(3,new Strategy3());

exige que vous et vos collègues implémentiez les fonctions / stratégies à appeler dans des classes distinctes, de manière plus uniforme (car vos objets de stratégie devront tous implémenter la même interface). Un tel code est souvent un peu plus complet que

     case 1:
          MyClass1.Doit1(someParameters);
          break;
     case 2:
          MyClass2.Doit2(someParameters);
          break;
     case 3:
          MyClass3.Doit3(someParameters);
          break;

Cependant, il ne vous libérera toujours pas du fardeau de la modification de ce fichier de code chaque fois qu'un nouveau numéro doit être ajouté. Les avantages réels de cette approche sont différents:

  • l'initialisation de la carte devient maintenant séparé du code d'expédition qui fait appelle la fonction associée à un numéro spécifique, et celui - ci ne pas contenir ces 50 répétitions plus, il suffit de regarder comme myMap[number].DoIt(someParameters). Ainsi, ce code de répartition n'a pas besoin d'être touché chaque fois qu'un nouveau numéro arrive et peut être mis en œuvre selon le principe Open-Closed. De plus, lorsque vous obtenez des exigences où vous devez étendre le code d'expédition lui-même, vous n'aurez plus à changer 50 emplacements, mais un seul.

  • le contenu de la carte est déterminé au moment de l'exécution (tandis que le contenu de la construction du commutateur est déterminé avant la compilation), ce qui vous donne la possibilité de rendre la logique d'initialisation plus flexible ou extensible.

Alors oui, il y a quelques avantages, et c'est sûrement un pas vers plus de code SOLIDE. Si cela rapporte au refactorisateur, c'est quelque chose que vous ou votre équipe devrez décider par vous-même. Si vous ne vous attendez pas à ce que le code de répartition soit modifié, que la logique d'initialisation soit modifiée et que la lisibilité de la switchne soit pas un réel problème, alors votre refactoring pourrait ne pas être si important maintenant.

Doc Brown
la source
Bien que je répugne à remplacer aveuglément chaque commutateur par du polymorphisme, je dirai que l'utilisation d'une carte comme Doc Brown le suggère ici a très bien fonctionné pour moi dans le passé. Lorsque vous implémentez la même interface, veuillez remplacer Doit1, Doit2etc. par une Doitméthode qui a de nombreuses implémentations différentes.
candied_orange
Et si vous avez le contrôle sur le type du symbole d'entrée utilisé comme clé, vous pouvez aller plus loin en créant doTheThing()une méthode du symbole d'entrée. Ensuite, vous n'avez pas besoin de maintenir la carte.
Kevin Krumwiede
1
@KevinKrumwiede: ce que vous proposez signifie simplement passer les objets de stratégie eux-mêmes dans le programme, en remplacement des entiers. Cependant, lorsque le programme prend un entier en entrée d'une source de données externe, il doit y avoir un mappage de l'entier à la stratégie associée au moins à un endroit du système.
Doc Brown le
En développant la suggestion de Doc Brown: vous pouvez également créer une fabrique qui contiendrait la logique de création des objets de stratégie, si vous décidez de suivre cette voie. Cela dit, la réponse fournie par CandiedOrange est la plus logique pour moi.
Vladimir Stokic
@DocBrown C'est à cela que je voulais en venir "si vous avez le contrôle sur le type du symbole d'entrée".
Kevin Krumwiede
0

Je suis fortement en faveur de la stratégie décrite dans la réponse de @DocBrown .

Je vais suggérer une amélioration de la réponse.

Les appels

 myMap.Add(1,new Strategy1());
 myMap.Add(2,new Strategy2());
 myMap.Add(3,new Strategy3());

peut être distribué. Vous n'avez pas besoin de revenir au même fichier pour ajouter une autre stratégie, qui adhère encore mieux au principe Open-Closed.

Supposons que vous implémentiez le Strategy1fichier Strategy1.cpp. Vous pouvez contenir le bloc de code suivant.

namespace Strategy1_Impl
{
   struct Initializer
   {
      Initializer()
      {
         getMap().Add(1, new Strategy1());
      }
   };
}
using namespace Strategy1_Impl;

static Initializer initializer;

Vous pouvez répéter le même code dans chaque fichier StategyN.cpp. Comme vous pouvez le voir, ce sera beaucoup de code répété. Pour réduire la duplication de code, vous pouvez utiliser un modèle qui peut être placé dans un fichier accessible à toutes les Strategyclasses.

namespace StrategyHelper
{
   template <int N, typename StrategyType> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new StrategyType());
      }
   };
}

Après cela, la seule chose que vous devez utiliser dans Strategy1.cpp est:

static StrategyHelper::Initializer<1, Strategy1> initializer;

La ligne correspondante dans StrategyN.cpp est:

static StrategyHelper::Initializer<N, StrategyN> initializer;

Vous pouvez amener l'utilisation des modèles à un autre niveau en utilisant un modèle de classe pour les classes de stratégie concrètes.

class Strategy { ... };

template <int N> class ConcreteStrategy;

Et puis, au lieu de Strategy1, utilisez ConcreteStrategy<1>.

template <> class ConcreteStrategy<1> : public Strategy { ... };

Modifiez la classe d'assistance pour enregistrer Strategys en:

namespace StrategyHelper
{
   template <int N> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new ConcreteStrategy<N>());
      }
   };
}

Modifiez le code dans Strateg1.cpp en:

static StrategyHelper::Initializer<1> initializer;

Changez le code dans StrategN.cpp en:

static StrategyHelper::Initializer<N> initializer;
R Sahu
la source