web-dev-qa-db-fra.com

Manières élégantes de gérer si (sinon) autre chose

C'est un petit problème, mais chaque fois que je dois coder quelque chose comme ça, la répétition me dérange, mais je ne suis pas sûr qu'aucune des solutions ne soit pire.

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
    }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}
  • Y a-t-il un nom pour ce genre de logique?
  • Suis-je un peu trop TOC?

Je suis ouvert aux suggestions de codes diaboliques, ne serait-ce que par curiosité ...

165
Benjol

Extrayez-le pour séparer la fonction (méthode) et utilisez l'instruction return:

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
        return;
    }
}

DefaultAction();

Ou, peut-être mieux, séparer le contenu et son traitement:

contents_t get_contents(name_t file)
{
    if(!FileExists(file))
        return null;

    contents = OpenFile(file);
    if(!SomeTest(contents)) // like IsContentsValid
        return null;

    return contents;
}

...

contents = get_contents(file)
contents ? DoSomething(contents) : DefaultAction();

Mise à jour:

Pourquoi pas des exceptions, pourquoi OpenFile ne lance pas IO exception:
Je pense que c'est une question vraiment générique, plutôt qu'une question sur le fichier IO. Des noms comme FileExists, OpenFile peuvent prêter à confusion, mais si les remplacer par Foo, Bar, etc., - il serait plus clair que DefaultAction peut être appelé aussi souvent que DoSomething, il peut donc s'agir d'un cas non exceptionnel. Péter Török a écrit à ce sujet à la fin de sa réponse

Pourquoi il y a opérateur conditionnel ternaire dans la 2ème variante:
S'il y avait une balise [C++], j'écrirais une instruction if avec une déclaration de contents dans sa partie condition:

if(contents_t contents = get_contents(file))
    DoSomething(contents);
else
    DefaultAction();

Mais pour les autres langages (de type C), if(contents) ...; else ...; est exactement la même que l'instruction d'expression avec opérateur conditionnel ternaire, mais plus longue. Parce que la partie principale du code était la fonction get_contents, Je viens d'utiliser la version plus courte (et j'ai également omis le type contents). Quoi qu'il en soit, c'est au-delà de cette question.

97
Abyx

Si le langage de programmation que vous utilisez (0) des comparaisons binaires de courts-circuits (c'est-à-dire si n'appelle pas SomeTest si FileExists renvoie false) et (1) l'affectation renvoie une valeur (le résultat de OpenFile est assigné à contents puis cette valeur est passée en argument à SomeTest), vous pouvez utiliser quelque chose comme ce qui suit, mais il vous serait quand même conseillé de commentez le code en notant que le single = est intentionnel.

if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}

Selon la complexité du if, il peut être préférable d'avoir une variable indicateur (qui sépare le test des conditions de réussite/d'échec avec le code qui gère l'erreur DefaultAction dans ce cas)

56
frozenkoi

Plus sérieusement que la répétition de l'appel à DefaultAction est le style lui-même car le code est écrit non orthogonal (voir cette réponse pour de bonnes raisons d'écrire orthogonalement).

Pour montrer pourquoi le code non orthogonal est mauvais, considérons l'exemple d'origine, lorsqu'une nouvelle exigence de ne pas ouvrir le fichier s'il est stocké sur un disque réseau est introduite. Eh bien, nous pourrions simplement mettre à jour le code comme suit:

if(FileExists(file))
{
    if(! OnNetworkDisk(file))
    {
        contents = OpenFile(file); // <-- prevents inclusion in if
        if(SomeTest(contents))
        {
            DoSomething(contents);
        }
        else
        {
            DefaultAction();
        }
    }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}

Mais il existe également une exigence selon laquelle nous ne devons pas non plus ouvrir de gros fichiers de plus de 2 Go. Eh bien, nous venons de mettre à jour à nouveau:

if(FileExists(file))
{
    if(LessThan2Gb(file))
    {
        if(! OnNetworkDisk(file))
        {
            contents = OpenFile(file); // <-- prevents inclusion in if
            if(SomeTest(contents))
            {
                DoSomething(contents);
            }
            else
            {
                DefaultAction();
            }
        }
        else
        {
            DefaultAction();
        }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}

Il devrait être très clair qu'un tel style de code sera un énorme problème de maintenance.

Parmi les réponses qui sont écrites correctement orthogonalement, il y a deuxième exemple d'Abyx et réponse de Jan Hudec , donc je ne répéterai pas cela, il suffit de souligner que l'ajout des deux exigences dans ces les réponses ne seraient que

if(! LessThan2Gb(file))
    return null;

if(OnNetworkDisk(file))
    return null;

(ou goto notexists; au lieu de return null;), n'affectant aucun autre code que les lignes ajoutées . Par exemple. orthogonal.

Lors des tests, la règle générale devrait être de tester les exceptions, pas le cas normal .

26
hlovdal

Évidemment:

Whatever(Arguments)
{
    if(!FileExists(file))
        goto notexists;
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(!SomeTest(contents))
        goto notexists;
    DoSomething(contents);
    return;
notexists:
    DefaultAction();
}

Vous avez dit que vous êtes ouvert même aux solutions perverses, alors en utilisant le mal, allez-y, non?

En fait, selon le contexte, cette solution pourrait bien être moins mauvaise que le mal faisant l'action deux fois ou la variable supplémentaire mal. Je l'ai enveloppé dans une fonction, car ce ne serait certainement pas OK au milieu d'une fonction longue (notamment en raison du retour au milieu). Mais la fonction longue n'est pas OK, point final.

Lorsque vous avez des exceptions, elles seront plus faciles à lire, surtout si vous pouvez faire en sorte que OpenFile et DoSomething lèvent simplement une exception si les conditions ne sont pas remplies, vous n'avez donc pas besoin du tout de vérifications explicites. En revanche, en C++, Java et C # lançant une exception est une opération lente, donc du point de vue des performances, le goto est toujours préférable.


Remarque sur le "mal": C++ FAQ 6.15 définit le "mal" comme:

Cela signifie que tel ou tel est quelque chose que vous devriez éviter la plupart du temps , mais pas quelque chose que vous devriez éviter tout le temps . Par exemple, vous finirez par utiliser ces choses "diaboliques" chaque fois qu'elles sont "la moins mauvaise des alternatives perverses".

Et cela s'applique à goto dans ce contexte. Les constructions de contrôle de flux structurées sont meilleures la plupart du temps, mais lorsque vous vous trouvez dans la situation où elles accumulent trop de leurs propres maux, comme l'affectation en condition, l'imbrication de plus de 3 niveaux environ, la duplication de code ou les conditions longues, goto peut finir par être moins maléfique.

25
Jan Hudec

Une possibilité:

boolean handled = false;

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
        handled = true;
    }
}
if (!handled)
{
    DefaultAction();
}

Bien sûr, cela rend le code légèrement plus complexe d'une manière différente. C'est donc en grande partie une question de style.

Une approche différente consisterait à utiliser des exceptions, par exemple:

try
{
    contents = OpenFile(file); // throws IO exception if file not found
    DoSomething(contents); // calls SomeTest() and throws exception on failure
}
catch(Exception e)
{
    DefaultAction();
    // and the exception should be at least logged...
}

Cela semble plus simple, mais il n'est applicable que si

  • nous savons précisément à quel type d’exceptions s’attendre, et DefaultAction() correspond à chaque
  • nous nous attendons à ce que le traitement de fichier réussisse, et un fichier manquant ou une SomeTest() défaillante est clairement une condition erronée, il est donc approprié de lever une exception dessus.
12
Péter Török
function FileContentsExists(file) {
    return FileExists(file) ? OpenFile(file) : null;
}

...

contents = FileContentExists(file);
if(contents && SomeTest(contents))
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}
12
herby

Les fonctions devraient faire une chose. Ils devraient bien le faire. Ils devraient le faire seulement.
- Robert Martin dans Clean Code

Certaines personnes trouvent cette approche un peu extrême, mais c'est aussi très propre. Permettez-moi d'illustrer en Python:

def processFile(self):
    if self.fileMeetsTest():
        self.doSomething()
    else:
        self.defaultAction()

def fileMeetsTest(self):
    return os.path.exists(self.path) and self.contentsTest()

def contentsTest(self):
    with open(self.path) as file:
        line = file.readline()
        return self.firstLineTest(line)

Quand il dit que les fonctions devraient faire une chose, il veut dire une chose. processFile() choisit une action en fonction du résultat d'un test, et c'est tout. fileMeetsTest() combine toutes les conditions du test, et c'est tout. contentsTest() transfère la première ligne vers firstLineTest(), et c'est tout.

Cela ressemble à beaucoup de fonctions, mais il se lit pratiquement comme un anglais simple:

Pour traiter le fichier, vérifiez s'il répond au test. Si c'est le cas, faites quelque chose. Sinon, effectuez l'action par défaut. Le fichier satisfait au test s'il existe et réussit le test de contenu. Pour tester le contenu, ouvrez le fichier et testez la première ligne. Le test de la première ligne ...

Certes, c'est un peu verbeux, mais notez que si un responsable ne se soucie pas des détails, il peut arrêter de lire après seulement les 4 lignes de code dans processFile(), et il aura toujours de bonnes connaissances de haut niveau de ce que fait la fonction.

11
Karl Bielefeldt

C'est à un niveau d'abstraction plus élevé:

if (WeCanDoSomething(file))
{
   DoSomething(contents);
}
else
{
   DefaultAction();
} 

Et cela remplit les détails.

boolean WeCanDoSomething(file)
{
    if FileExists(file)
    {
        contents = OpenFile(file);
        return (SomeTest(contents));
    }
    else
    {
        return FALSE;
    }
}
11
Jeanne Pindar

En ce qui concerne ce que cela s'appelle, il peut facilement se développer en anti-motif de pointe de flèche à mesure que votre code se développe pour gérer plus d'exigences (comme indiqué par la réponse fournie) à https://softwareengineering.stackexchange.com/a/122625/33922 ) puis tombe dans le piège d'avoir d'énormes sections de codes avec des instructions conditionnelles imbriquées qui ressemblent à une flèche.

Voir des liens tels que;

http://codinghorror.com/blog/2006/01/flattening-arrow-code.html

http://lostechies.com/chrismissal/2009/05/27/anti-patterns-and-worst-practices-the-arrowhead-anti-pattern/

Il y a beaucoup plus à ce sujet et d'autres anti-modèles sur Google.

Voici quelques bons conseils que Jeff donne sur son blog à ce sujet;

1) Remplacer les conditions par des clauses de garde.

2) Décomposer les blocs conditionnels en fonctions distinctes.

3) Convertissez les chèques négatifs en chèques positifs

4) Revenez toujours opportuniste dès que possible de la fonction.

Voir aussi certains commentaires sur le blog de Jeff concernant les suggestions de Steve McConnells sur les retours anticipés;

"Utilisez un retour lorsqu'il améliore la lisibilité: dans certaines routines, une fois que vous connaissez la réponse, vous souhaitez la renvoyer immédiatement à la routine appelante. Si la routine est définie de telle manière qu'elle ne nécessite aucun nettoyage supplémentaire une fois qu'elle est détecte une erreur, ne pas retourner immédiatement signifie que vous devez écrire plus de code. "

...

"Minimisez le nombre de retours dans chaque routine: il est plus difficile de comprendre une routine lorsque, en la lisant en bas, vous n'êtes pas conscient de la possibilité qu'elle soit retournée quelque part. Pour cette raison, utilisez les retours judicieusement - uniquement lorsqu'ils s'améliorent lisibilité. "

J'ai toujours souscrit à la théorie de 1 entrée/sortie par fonction en raison de ce que j'ai appris il y a environ 15 ans. Je pense que cela rend le code beaucoup plus facile à lire et comme vous le mentionnez plus maintenable

6
Mr Moose

Ceci est conforme aux règles DRY, no-goto et no-multiple-return, est évolutif et lisible à mon avis:

success = FileExists(file);
if (success)
{
    contents = OpenFile(file);
    success = SomeTest(contents);
}
if (success)
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}
6
mouviciel

Pour ce cas particulier, la réponse est assez simple ...

Il existe une condition de concurrence entre FileExists et OpenFile: que se passe-t-il si le fichier est supprimé?

La seule façon sensée de traiter ce cas particulier est de sauter FileExists:

contents = OpenFile(file);
if (!contents) // open failed
    DefaultAction();
else (SomeTest(contents))
    DoSomething(contents);

Cela résout parfaitement ce problème et rend le code plus propre.

En général: Essayez de repenser le problème et imaginez une autre solution qui évite complètement le problème.

3
Martin Wickman

Je l'extrait dans une méthode distincte, puis:

if(!FileExists(file))
{
    DefaultAction();
    return;
}

contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return;
}

DoSomething(contents);

ce qui permet également

if(!FileExists(file))
{
    DefaultAction();
    return Result.FileNotFound;
}

contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return Result.TestFailed;
}

DoSomething(contents);
return Result.Success;            

alors vous pourriez peut-être supprimer les appels DefaultAction et laisser l'exécution de DefaultAction à l'appelant:

Result OurMethod(file)
{
    if(!FileExists(file))
    {
        return Result.FileNotFound;
    }

    contents = OpenFile(file);
    if(!SomeTest(contents))
    {
        return Result.TestFailed;
    }

    DoSomething(contents);
    return Result.Success;            
}

void Caller()
{
    // something, something...

    var result = OurMethod(file);
    // if (result == Result.FileNotFound || result == Result.TestFailed), or just
    if (result != Result.Success)        
    {
        DefaultAction();
    }
}

J'aime l'approche de Jeanne Pindar aussi.

3
Konrad Morawski

Une autre possibilité si vous n'aimez pas en voir trop est de supprimer complètement l'utilisation de else et d'ajouter une déclaration de retour supplémentaire. Else est un peu superflu à moins que vous ayez besoin d'une logique plus complexe pour déterminer s'il y a plus que deux possibilités d'action.

Ainsi, votre exemple pourrait devenir:

void DoABunchOfStuff()
{
    if(FileExists(file))
    {
        DoSomethingWithFileContent(file);
        return;
    }

    DefaultAction();
}

void DoSomethingWithFileContent(file)
{        
    var contents = GetFileContents(file)

    if(SomeTest(contents))
    {
        DoSomething(contents);
        return;
    }

    DefaultAction();
}

AReturnType GetFileContents(file)
{
    return OpenFile(file);
}

Personnellement, cela ne me dérange pas d'utiliser la clause else car elle indique explicitement comment la logique est censée fonctionner, et améliore ainsi la lisibilité de votre code. Certains outils d'embellissement de code préfèrent toutefois simplifier en une seule instruction if pour décourager la logique d'imbrication.

2
S.Robins

Le cas illustré dans l'exemple de code peut généralement être réduit à une seule instruction if. Sur de nombreux systèmes, la fonction d'ouverture de fichier renvoie une valeur non valide si le fichier n'existe pas déjà. Parfois, c'est le comportement par défaut; d'autres fois, il doit être spécifié via un argument. Cela signifie que le test FileExists peut être abandonné, ce qui peut également aider avec les conditions de concurrence résultant de la suppression de fichier entre le test d'existence et l'ouverture de fichier.

file = OpenFile(path);
if(isValidFileHandle(file) && SomeTest(file)) {
    DoSomething(file);
} else {
    DefaultAction();
}

Cela ne résout pas directement le problème de mélange au niveau de l'abstraction car il évite complètement le problème de plusieurs tests non chaînables, bien que la suppression du test d'existence de fichier ne soit pas incompatible avec la séparation des niveaux d'abstraction. En supposant que les descripteurs de fichiers non valides sont équivalents à "faux" et que les descripteurs de fichiers se ferment lorsqu'ils sortent du domaine:

OpenFileIfSomething(path:String) : FileHandle {
    file = OpenFile(path);
    if (file && SomeTest(file)) {
        return file;
    }
    return null;
}

...

if ((file = OpenFileIfSomething(path))) {
    DoSomething(file);
} else {
    DefaultAction();
}
2
outis

Je suis d'accord avec freezkoi, cependant, pour C # de toute façon, je pensais que cela aiderait à suivre la syntaxe des méthodes TryParse.

if(FileExists(file) && TryOpenFile(file, out contents))
    DoSomething(contents);
else
    DefaultAction();
bool TryOpenFile(object file, out object contents)
{
    try{
        contents = OpenFile(file);
    }
    catch{
        //something bad happened, computer probably exploded
        return false;
    }
    return true;
}
2
Biff MaGriff

Qu'est-ce qui ne va pas avec l'évidence

if(!FileExists(file)) {
    DefaultAction();
    return;
}
contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return;
}        
DoSomething(contents);

Cela me semble assez standard? Pour ce genre de grande procédure où beaucoup de petites choses doivent se produire, dont l'échec empêcherait ce dernier. Les exceptions le rendent un peu plus propre si c'est une option.

1
Steve Bennett

Bien sûr, vous ne pouvez aller aussi loin que dans des scénarios comme ceux-ci, mais voici un chemin à parcourir:

interface File<T> {
    function isOK():Bool;
    function getData():T;
}

var appleFile:File<Apple> = appleStorage.get(fileURI);
if (appleFile.isOK())
    eat(file.getData());
else
    cry();

Vous voudrez peut-être des filtres supplémentaires. Ensuite, faites ceci:

var appleFile = appleStorage.get(fileURI, isEdible);
//isEdible is of type Apple->Bool and will be used internally to answer to the isOK call
if (appleFile.isOK())
    eat(file.getData());
else
    cry();

Bien que cela puisse aussi avoir du sens:

function eat(Apple:Apple) {
     if (isEdible(Apple)) 
         digest(Apple);
     else
         die();
}
var appleFile = appleStorage.get(fileURI);
if (appleFile.isOK())
    eat(appleFile.getData());
else
    cry();

Quel est le meilleur? Cela dépend du problème du monde réel auquel vous êtes confronté.
Mais la chose à retenir est que vous pouvez faire beaucoup avec la composition et le polymorphisme.

1
back2dos

Votre code est moche parce que vous en faites trop dans une seule fonction. Vous voulez soit traiter le fichier, soit effectuer l'action par défaut, alors commencez par dire que:

if (!ProcessFile(file)) { 
  DefaultAction(); 
}

Perl et Ruby écrivent processFile(file) || defaultAction()

Maintenant, écrivez ProcessFile:

if (FileExists(file)) { 
  contents = OpenFile(file);
  if (SomeTest(contents)) {
    processContents(contents);
    return true;
  }
}
return false;
1
kevin cline

De toute évidence, la solution la plus élégante et concise consiste à utiliser une macro de préprocesseur.

#define DOUBLE_ELSE(CODE) else { CODE } } else { CODE }

Ce qui vous permet d'écrire du beau code comme celui-ci:

if(FileExists(file))
{
    contents = OpenFile(file);
    if(SomeTest(contents))
    {
        DoSomething(contents);
    }
    DOUBLE_ELSE(DefaultAction();)

Il peut être difficile de s'appuyer sur le formatage automatique si vous utilisez souvent cette technique, et certains IDE peuvent vous crier un peu sur ce qu'il suppose à tort être mal formé. Et comme le dit le proverbe, tout est un compromis, mais je suppose que ce n'est pas un mauvais prix à payer pour éviter les maux du code répété.

0
Peter Olson

Pour réduire les IF imbriqués:

1/retour anticipé;

2/expression composée (sensible aux courts-circuits)

Ainsi, votre exemple peut être refactorisé comme ceci:

if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
    DoSomething(contents);
    return;
}
DefaultAction();
0
DQ_vn

Je reconnais que c'est une vieille question, mais j'ai remarqué un modèle qui n'a pas été mentionné; principalement, définir une variable pour déterminer ultérieurement la ou les méthodes que vous souhaitez appeler (en dehors de if ... else ...).

C'est juste un autre angle à regarder pour rendre le code plus facile à utiliser. Il vous permet également d'ajouter une autre méthode ou de changer la méthode appropriée qui doit être appelée dans certaines situations.

Plutôt que de devoir remplacer toutes les mentions de la méthode (et peut-être manquer certains scénarios), elles sont toutes répertoriées à la fin du bloc if ... else ... et sont plus simples à lire et à modifier. J'ai tendance à l'utiliser lorsque, par exemple, plusieurs méthodes peuvent être appelées, mais dans le cas où ... sinon ... une méthode peut être appelée en plusieurs correspondances.

Si vous définissez une variable qui définit l'état, vous pouvez avoir de nombreuses options profondément imbriquées et mettre à jour l'état lorsque quelque chose doit être (ou ne pas être) effectué.

Cela pourrait être utilisé comme dans l'exemple demandé dans la question où nous vérifions si "DoSomething" s'est produit, et sinon, effectuez l'action par défaut. Ou vous pouvez avoir un état pour chaque méthode que vous souhaitez appeler, définir le cas échéant, puis appeler la méthode applicable en dehors de if ... else ...

À la fin des instructions imbriquées if ... else ..., vous vérifiez l'état et agissez en conséquence. Cela signifie que vous n'avez besoin que d'une seule mention d'une méthode au lieu de tous les emplacements auxquels elle doit être appliquée.

bool ActionDone = false;

if (Method_1(object_A)) // Test 1
{
    result_A = Method_2(object_A); // Result 1

    if (Method_3(result_A)) // Test 2
    {
        Method_4(result_A); // Action 1
        ActionDone = true;
    }
}

if (!ActionDone)
{
    Method_5(); // Default Action
}
0
Steve Padmore

J'ai vu beaucoup d'exemples avec "return" que j'utilise aussi mais parfois je veux éviter de créer de nouvelles fonctions et utiliser une boucle à la place:

while (1) {
    if (FileExists(file)) {
        contents = OpenFile(file);
        if (SomeTest(contents)) {
           DoSomething(contents);
           break;
        } 
    }
    DefaultAction();
    break;
}

Si vous voulez écrire moins de lignes ou que vous détestez les boucles infinies comme moi, vous pouvez changer le type de boucle en "do ... while (0)" et éviter le dernier "break".

0
XzKto

Que diriez-vous de cette solution:

content = NULL; //I presume OpenFile returns a pointer 
if(FileExists(file))
    contents = OpenFile(file);
if(content != NULL && SomeTest(contents))
    DoSomething(contents);
else
    DefaultAction();

J'ai fait l'hypothèse qu'OpenFile renvoie un pointeur, mais cela pourrait également fonctionner avec le type de valeur return en spécifiant une valeur par défaut non renvoyable (codes d'erreur ou quelque chose comme ça).

Bien sûr, je ne m'attends pas à une action possible via la méthode SomeTest sur le pointeur NULL (mais vous ne savez jamais), donc cela pourrait également être considéré comme une vérification supplémentaire du pointeur NULL pour l'appel SomeTest (contents).

0
chedi