web-dev-qa-db-fra.com

Comment refactoriseriez-vous les relevés IF imbriqués?

Je naviguais autour de la blogosphère de programmation quand je suis tombé sur ce post sur GOTO:

http://giuliozambon.blogspot.com/2010/12/programmers-tabu.html

Ici, l'écrivain explique comment "on doit arriver à la conclusion qu'il existe des situations où les GOTO créent un code plus lisible et plus maintenable", puis continue en montrant un exemple similaire à celui-ci:

if (Check#1)
{
    CodeBlock#1
    if (Check#2)
    {
        CodeBlock#2
        if (Check#3)
        {
            CodeBlock#3
            if (Check#4)
            {
                CodeBlock#4
                if (Check#5)
                {
                    CodeBlock#5
                    if (Check#6)
                    {
                        CodeBlock#6
                        if (Check#7)
                        {
                            CodeBlock#7
                        }
                        else
                        {
                            rest - of - the - program
                        }
                    }
                }
            }
        }
    }
}

L'auteur propose ensuite que l'utilisation de GOTO rendrait ce code beaucoup plus facile à lire et à maintenir.

Personnellement, je peux penser à au moins 3 façons différentes de l'aplatir et de rendre ce code plus lisible sans recourir à des GOTO révolutionnaires. Voici mes deux favoris.

1 - Petites fonctions imbriquées. Prenez chaque if et son bloc de code et transformez-le en fonction. Si la vérification booléenne échoue, revenez simplement. S'il passe, appelez la fonction suivante de la chaîne. (Garçon, cela ressemble beaucoup à la récursivité, pourriez-vous le faire en une seule boucle avec des pointeurs de fonction?)

2 - Variable Sentinelle. Pour moi, c'est le plus simple. Utilisez simplement une variable blnContinueProcessing et vérifiez si elle est toujours vraie dans votre vérification if. Ensuite, si la vérification échoue, définissez la variable sur false.

De combien de manières différentes ce type de problème de codage peut-il être refactorisé pour réduire l'imbrication et augmenter la maintenabilité?

34
saunderl

C'est vraiment difficile à dire sans savoir comment les différents contrôles interagissent. Une refonte rigoureuse pourrait être de mise. La création d'une topologie d'objets qui exécutent le bloc correct en fonction de leur type, également un modèle de stratégie ou un modèle d'état pourrait faire l'affaire.

Sans savoir quoi faire de mieux, je considérerais deux refactorings simples possibles qui pourraient être encore refactorisés en extrayant plus de méthodes.

Le premier que je n'aime pas vraiment car j'aime toujours comme petits points de sortie dans une méthode (de préférence une)

if (!Check#1)
{ 
    return;
}
CodeBlock#1

if (!Check#2)
{
    return;
}
CodeBlock#2
...

Le second supprime les multiples retours mais ajoute également beaucoup de bruit. (il supprime simplement l'imbrication)

bool stillValid = Check#1
if (stillValid)
{
  CodeBlock#1
}

stillValid = stillValid && Check#2
if (stillValid)
{
  CodeBlock#2
}

stillValid = stillValid && Check#3
if (stillValid)
{
  CodeBlock#3
}
...

Ce dernier peut être refactorisé joliment en fonctions et lorsque vous leur donnez de bons noms, le résultat peut être raisonnable ';

bool stillValid = DoCheck1AndCodeBlock1()
stillValid = stillValid && DoCheck2AndCodeBlock2()
stillValid = stillValid && DoCheck3AndCodeBlock3()

public bool DoCheck1AndCodeBlock1()
{
   bool returnValid = Check#1
   if (returnValid)
   {
      CodeBlock#1
   }
   return returnValid
}

Dans l'ensemble, il existe probablement de meilleures options

33
KeesDijk

Cela s'appelle "Arrow Code" en raison de la forme du code avec une indentation appropriée.

Jeff Atwood a publié un bon article de blog sur Coding Horror sur la façon d'aplatir les flèches:

Code de flèche aplatie

Lisez l'article pour le traitement complet, mais voici les principaux points ..

  • Remplacer les conditions par des clauses de garde
  • Décomposer les blocs conditionnels en fonctions distinctes
  • Convertissez les chèques négatifs en chèques positifs
  • Revenez toujours opportuniste dès que possible de la fonction
40
JohnFx

Je sais que certaines personnes diront que c'est un goto, mais return; est le refactoring évident, c'est-à-dire.

if (!Check#1)
{ 
        return;
}
CodeBlock#1
if (!Check#2)
{
    return;
}
CodeBlock#2
.
.
.
if (Check#7)
{
    CodeBlock#7
}
else
{
    rest - of - the - program
}

Si ce n'est vraiment qu'un tas de vérifications de garde avant d'exécuter le code, cela fonctionne bien. Si c'est plus compliqué que cela, cela ne fera que le rendre un peu plus simple, et vous avez besoin de l'une des autres solutions.

26
Richard Gadsden

Ce code spaghetti semble être le candidat parfait pour le refactoriser dans une machine à états.

10
Pemdas

Cela a peut-être déjà été mentionné, mais ma réponse "go-to" (jeu de mots voulu) à "l'anti-modèle de tête de flèche" montré ici est d'inverser les ifs. Testez l'opposé des conditions actuelles (assez facile avec un opérateur not) et revenez de la méthode si c'est vrai. Pas de gotos (bien que pédantiquement parlant, un retour nul est un peu plus qu'un saut à la ligne de code appelant, avec l'étape supplémentaire triviale de faire sauter un cadre de la pile).

En voici un exemple:

if (Check#1)
{
    CodeBlock#1
    if (Check#2)
    {
        CodeBlock#2
        if (Check#3)
        {
            CodeBlock#3
            if (Check#4)
            {
                CodeBlock#4
                if (Check#5)
                {
                    CodeBlock#5
                    if (Check#6)
                    {
                        CodeBlock#6
                        if (Check#7)
                        {
                            CodeBlock#7
                        }
                        else
                        {
                            rest - of - the - program
                        }
                    }
                }
            }
        }
    }
}

Refactorisé:

if (!Check#1) return;

CodeBlock#1

if (!Check#2) return;

CodeBlock#2

if (!Check#3) return;

CodeBlock#3

if (!Check#4) return;

CodeBlock#4

if (!Check#5) return;

CodeBlock#5

if (!Check#6) return;

CodeBlock#6

if (Check#7)
    CodeBlock#7
else
{
    //rest of the program
}

À chaque if, nous vérifions essentiellement si nous devons continuer. Il fonctionne exactement de la même manière que l'original avec un seul niveau d'imbrication.

S'il y a quelque chose au-delà de la fin de cet extrait qui doit également être exécuté, extrayez ce code dans sa propre méthode et appelez-le d'où qu'il se trouve actuellement, avant de passer au code qui viendrait après cet extrait. L'extrait lui-même est suffisamment long, étant donné un LOC réel suffisant dans chaque bloc de code, pour justifier le fractionnement de plusieurs autres méthodes, mais je m'éloigne du sujet.

6
KeithS

Si vous avez régulièrement une logique qui nécessite réellement cette pyramide de vérifications if, vous utilisez probablement (métaphoriquement) une clé pour marteler les ongles. Vous seriez mieux servi en faisant ce genre de logique compliquée et tordue dans un langage qui prend en charge ce type de logique compliquée et tordue avec de meilleures structures que linéaire if/else if/else- constructions de style.

Les langues qui pourraient être mieux adaptées à ce type de structure pourraient inclure SNOBOL4 (avec son bizarre branchement de style dual-GOTO) ou des langages logiques comme Prolog et Mercury (avec leurs capacités d'unification et de retour en arrière, sans parler de leurs DCG pour une expression plutôt succincte de décisions compliquées) .

Bien sûr, si ce n'est pas une option (parce que la plupart des programmeurs ne sont malheureusement pas polyglottes), les idées que les autres ont proposées sont bonnes, comme utiliser divers OOP - structures ou décomposition des clauses en fonctions ou même, si vous êtes désespéré, et ne vous occupez pas du fait que la plupart des gens les trouvent illisibles, en utilisant un machine d'état .

Ma solution, cependant, reste d'atteindre un langage qui vous permet d'exprimer la logique que vous essayez d'exprimer (en supposant que cela soit courant dans votre code) d'une manière plus facile et plus lisible.

De ici :

Très souvent, lorsque je regarde un ensemble de pac-man, si je trouve que si je dessine quelque chose comme une table de vérité de toutes les conditions impliquées, je peux trouver un bien meilleur moyen de résoudre le problème.

De cette façon, vous pouvez également évaluer s'il existe une meilleure méthode, comment vous pouvez la décomposer davantage et (et c'est un grand avec ce type de code) s'il y a des trous dans la logique.

Cela fait, vous pouvez probablement le décomposer en quelques instructions de commutation et quelques méthodes et sauver le prochain pauvre coupable qui doit passer par le code beaucoup de problèmes.

1
Jim G.

Personnellement, j'aime emballer ces instructions if dans des fonctions distinctes qui renvoient un booléen si la fonction réussit.

La structure se présente comme suit:


if (DoCheck1AndCodeBlock1() && 
    DoCheck2AndCodeBlock2() && 
    DoCheck3AndCodeBlock3()) 
{
   // ... you may perform the final operation here ....
}

Le seul inconvénient est que ces fonctions devront générer des données à l'aide d'attributs transmis par référence ou par des variables membres.

1
gogisan

En utilisant une approche OO un modèle composite où leaf est une condition simple et un composant une union d'éléments simples rendent ce type de code extensible et adaptable

1
guiman

Si vous voulez une solution orientée objet possible, le code ressemble à un exemple canonique de refactorisation en utilisant le modèle de conception de chaîne de responsabilité .

0
FinnNk

Les diviser en fonctions pourrait aider?

Vous appelez la première fonction et quand elle se terminera, elle appellera la suivante, la première chose que la fonction ferait serait de tester la vérification de cette fonction.

0
Toby

Cela dépend beaucoup de la taille de chaque bloc de code et de la taille totale, mais j'ai tendance à diviser soit par une "méthode d'extraction" directe sur un bloc, soit en déplaçant les parties inconditionnelles dans des méthodes distinctes. Mais les mises en garde @KeesDijk mentionnées s'appliquent. L'ancienne approche vous donne une série de fonctions comme

if (Check#1)
{
    CodeBlock#1
    NextFunction
}

Ce qui peut bien fonctionner, mais conduit à un gonflement du code et à l'anti-modèle "méthode utilisée une seule fois". Je préfère donc généralement cette approche:

if (CheckFunctionOne)
{
    MethodOneWithDescriptiveName
    if (CheckFunctionTwo)
    {
        MethodTwoWithDescriptiveName
        ....

Avec une utilisation appropriée des variables privées et le passage des paramètres, cela peut être fait. Ayant jeté un coup d'œil à la programmation alphabétisée peut aider ici.

0
Мסž