web-dev-qa-db-fra.com

Comment puis-je modifier une chaîne d'instructions if-else if pour adhérer aux principes Clean Code de l'oncle Bob?

J'essaie de suivre les suggestions de code propre d'Oncle Bob et en particulier de garder les méthodes courtes.

Je ne parviens cependant pas à raccourcir cette logique:

if (checkCondition()) {addAlert(1);}
else if (checkCondition2()) {addAlert(2);}
else if (checkCondition3()) {addAlert(3);}
else if (checkCondition4()) {addAlert(4);}

Je ne peux pas supprimer les elses et ainsi séparer le tout en bits plus petits, provoquer le "else" dans le "else if" aide les performances - l'évaluation de ces conditions est coûteuse et si je peux éviter d'évaluer les conditions ci-dessous, provoquer l'une des premières c'est vrai, je veux les éviter.

Même sémantiquement parlant, évaluer la condition suivante si la précédente était remplie n'a pas de sens du point de vue commercial.


edit: Cette question a été identifiée comme un doublon possible de façons élégantes de gérer si (sinon) .

Je pense que c'est une question différente (vous pouvez le voir également en comparant les réponses à ces questions).

45
Ev0oD

Idéalement, je pense que vous devriez extraire votre logique pour obtenir le code/numéro d'alerte dans sa propre méthode. Ainsi, votre code existant est réduit jusqu'à

{
    addAlert(GetConditionCode());
}

et vous avez GetConditionCode () encapsuler la logique de vérification des conditions. Peut-être aussi préférable d'utiliser un Enum qu'un nombre magique.

private AlertCode GetConditionCode() {
    if (CheckCondition1()) return AlertCode.OnFire;
    if (CheckCondition2()) return AlertCode.PlagueOfBees;
    if (CheckCondition3()) return AlertCode.Godzilla;
    if (CheckCondition4()) return AlertCode.ZombieSharkNado;
    return AlertCode.None;
}
81
Steven Eccles

La mesure importante est la complexité du code, pas la taille absolue. En supposant que les différentes conditions ne sont en réalité que des appels de fonction uniques, tout comme les actions ne sont pas plus complexes que ce que vous avez montré, je dirais qu'il n'y a rien de mal avec le code. C'est déjà aussi simple que possible.

Toute tentative de "simplifier" davantage compliquera les choses.

Bien sûr, vous pouvez remplacer le mot clé else par un return comme d'autres l'ont suggéré, mais ce n'est qu'une question de style, et non un changement de complexité.


De côté:

Mon conseil général serait de ne jamais devenir religieux au sujet d'une règle de code propre: la plupart des conseils de codage que vous voyez sur Internet sont bons s'ils sont appliqués dans un contexte approprié, mais en appliquant radicalement ce même conseil partout, vous pouvez gagner une entrée dans le IOCCC . L'astuce est toujours de trouver un équilibre qui permet aux êtres humains de raisonner facilement sur votre code.

Utilisez des méthodes trop grosses et vous êtes foutu. Utilisez des fonctions trop petites et vous êtes foutu. Évitez les expressions ternaires et vous êtes foutu. Utilisez des expressions ternaires partout et vous êtes foutu. Sachez qu'il y a des endroits qui appellent des fonctions d'une ligne et des endroits qui appellent des fonctions de 50 lignes (oui, ils existent!). Sachez qu'il y a des endroits qui appellent des instructions if() et qu'il y a des endroits qui appellent les ?: opérateur. Utilisez l'arsenal complet à votre disposition et essayez de toujours utiliser l'outil le plus approprié que vous pouvez trouver. Et rappelez-vous, ne soyez pas religieux même à propos de ces conseils.

Il est controversé de savoir si cela est "meilleur" que le simple si ... sinon pour un cas donné. Mais si vous voulez essayer autre chose, c'est une façon courante de le faire.

Mettez vos conditions dans des objets et mettez ces objets dans une liste

foreach(var condition in Conditions.OrderBy(i=>i.OrderToRunIn))
{
    if(condition.EvaluatesToTrue())
    {
        addAlert(condition.Alert);
        break;
    }
}

Si plusieurs actions sont requises à condition que vous puissiez faire une récursion folle

void RunConditionalAction(ConditionalActionSet conditions)
{
    foreach(var condition in conditions.OrderBy(i=>i.OrderToRunIn))
    {
        if(condition.EvaluatesToTrue())
        {
            RunConditionalAction(condition);
            break;
        }
    }
}

Évidemment oui. Cela ne fonctionne que si vous avez un modèle dans votre logique. Si vous essayez de créer une action conditionnelle récursive super générique, la configuration de l'objet sera aussi compliquée que l'instruction if d'origine. Vous inventerez votre propre nouveau langage/framework.

Mais votre exemple fait a un modèle

Un cas d'utilisation courant pour ce modèle serait la validation. Au lieu de :

bool IsValid()
{
    if(condition1 == false)
    {
        throw new ValidationException("condition1 is wrong!");
    }
    elseif(condition2 == false)
    {
    ....

}

Devient

[MustHaveCondition1]
[MustHaveCondition2]
public myObject()
{
    [MustMatchRegExCondition("xyz")]
    public string myProperty {get;set;}
    public bool IsValid()
    {
        conditions = getConditionsFromReflection()
        //loop through conditions
    }
}
22
Ewan

Pensez à utiliser return; Après la réussite d'une condition, cela vous permet d'économiser tous les elses. Vous pourriez même être en mesure de return addAlert(1) directement si cette méthode a une valeur de retour.

7
Kilian Foth

J'ai parfois vu des constructions comme celle-ci considérées comme plus propres:

switch(true) {
    case cond1(): 
        statement1; break;
    case cond2():
        statement2; break;
    case cond3():
        statement3; break;
    // .. etc
}

Le ternaire avec un bon espacement peut également être une alternative intéressante:

cond1() ? statement1 :
cond2() ? statement2 :
cond3() ? statement3 : (null);

Je suppose que vous pouvez également essayer de créer un tableau avec une paire contenant une condition et une fonction et l'itérer jusqu'à ce que la première condition soit remplie - ce qui, comme je le vois, serait égal à la première réponse d'Ewan.

5
zworek

En variante de la réponse de @ Ewan, vous pouvez créer une chaîne (au lieu d'une "liste plate") de conditions comme celle-ci:

abstract class Condition {
  private static final  Condition LAST = new Condition(){
     public void alertOrPropagate(DisplayInterface display){
        // do nothing;
     }
  }
  private Condition next = Last;

  public Condition setNext(Condition next){
    this.next = next;
    return this; // fluent API
  }

  public void alertOrPropagate(DisplayInterface display){
     if(isConditionMeet()){
         display.alert(getMessage());
     } else {
       next.alertOrPropagate(display);
     }
  }
  protected abstract boolean isConditionMeet();
  protected abstract String getMessage();  
}

De cette façon, vous pouvez appliquer vos conditions dans un ordre défini et l'infrastructure (la classe abstraite montrée) ignore les vérifications restantes une fois la première remplie.

C'est là qu'il est supérieur à l'approche de la "liste plate" où vous devez implémenter le "saut" dans la boucle qui applique les conditions.

Vous configurez simplement la chaîne de conditions:

Condition c1 = new Condition1().setNext(
  new Condition2().setNext(
   new Condition3()
 )
);

Et commencez l'évaluation par un simple appel:

c1.alertOrPropagate(display);
1
Timothy Truckle

Je ne peux pas parler de votre situation particulière car le code n'est pas spécifique, mais ...

un code comme celui-ci est souvent une odeur pour un modèle OO manquant. Vous avez vraiment quatre types de choses, chacune associée à son propre type d'alerte, mais plutôt que de reconnaître ces entités et de créer une instance de classe pour chacun, vous les traitez comme une chose et vous essayez de vous rattraper plus tard, à un moment où vous avez vraiment besoin de savoir à quoi vous avez affaire pour continuer.

Le polymorphisme vous a peut-être mieux convenu.

Méfiez-vous du code avec des méthodes longues contenant des constructions if-then longues ou complexes. Vous voulez souvent un arbre de classe avec quelques méthodes virtuelles.

0
Martin Maat

Tout d'abord, le code d'origine n'est pas terrible IMO. C'est assez compréhensible et il n'y a rien de mauvais en soi.

Ensuite, si vous ne l'aimez pas, construisez sur l'idée de @ Ewan d'utiliser une liste mais en supprimant son modèle quelque peu contre nature foreach break:

public class conditions
{
    private List<Condition> cList;
    private int position;

    public Condition Head
    {
        get { return cList[position];}
    }

    public bool Next()
    {
        return (position++ < cList.Count);
    }
}


while not conditions.head.check() {
  conditions.next()
}
conditions.head.alert()

Adaptez maintenant ceci dans la langue de votre choix, faites de chaque élément de la liste un objet, un Tuple, peu importe, et vous êtes bon.

EDIT: il semble que ce ne soit pas aussi clair que je le pensais, alors laissez-moi vous expliquer davantage. conditions est une liste ordonnée quelconque; head est l'élément en cours d'étude - au début, c'est le premier élément de la liste, et chaque fois que next() est appelée, elle devient la suivante; check() et alert() sont les checkConditionX() et addAlert(X) de l'OP.

0
Nico

En supposant que toutes les fonctions sont implémentées dans le même composant, vous pouvez faire en sorte que les fonctions conservent un état afin de se débarrasser des multiples branches du flux.

EG: checkCondition1() deviendrait evaluateCondition1(), sur laquelle il vérifierait si la condition précédente était remplie; si c'est le cas, il met en cache une valeur à récupérer par getConditionNumber().

checkCondition2() deviendrait evaluateCondition2(), sur laquelle il vérifierait si les conditions précédentes étaient remplies. Si la condition précédente n'était pas remplie, il vérifie le scénario de condition 2, mettant en cache une valeur à récupérer par getConditionNumber(). Etc.

clearConditions();
evaluateCondition1();
evaluateCondition2();
evaluateCondition3();
evaluateCondition4();
if (anyCondition()) { addAlert(getConditionNumber()); }

MODIFIER:

Voici comment le contrôle des conditions coûteuses devrait être mis en œuvre pour que cette approche fonctionne.

bool evaluateCondition34() {
    if (!anyCondition() && A && B && C) {
        conditionNumber = 5693;
        return true;
    }
    return false;
}

...

bool evaluateCondition76() {
    if (!anyCondition() && !B && C && D) {
        conditionNumber = 7658;
        return true;
    }
    return false;
}

Par conséquent, si vous avez trop de contrôles coûteux à effectuer et que les éléments de ce code restent privés, cette approche permet de le maintenir, ce qui permet de modifier l'ordre des contrôles si nécessaire.

clearConditions();
evaluateCondition10();
evaluateCondition9();
evaluateCondition8();
evaluateCondition7();
...
evaluateCondition34();
...
evaluateCondition76();

if (anyCondition()) { addAlert(getConditionNumber()); }

Cette réponse fournit simplement une suggestion alternative des autres réponses, et ne sera probablement pas meilleure que le code d'origine si nous considérons seulement 4 lignes de code. Bien que cela ce n'est pas une approche terrible (et ni l'un ni l'autre ne rend la maintenance plus difficile comme d'autres l'ont dit) étant donné le scénario que j'ai mentionné (trop de vérifications, seule la fonction principale est présentée comme publique, toutes les fonctions sont des détails d'implémentation de la même classe).

0
Emerson Cardoso

La question manque de détails. Si les conditions sont:

  • sous réserve de modifications ou
  • répété dans d'autres parties de l'application ou du système ou
  • modifié dans certains cas (comme différentes versions, tests, déploiements)

ou si le contenu de addAlert est plus compliqué, alors une meilleure solution possible en disons c # serait:

//in some central spot
IEnumerable<Tuple<Func<bool>, int>> Conditions = new ... {
  Tuple.Create(CheckCondition1, 1),
  Tuple.Create(CheckCondition2, 2),
  ...
}

//at the original place
var matchingCondition = Conditions.Where(c=>c.Item1()).FirstOrDefault();
if(matchingCondition != null) 
  addAlert(matchingCondition.Item2)

Les tuples ne sont pas si beaux en c # <8, mais choisis par convenance.

Les avantages de cette méthode, même si aucune des options ci-dessus ne s'applique, est que la structure est typée statiquement. Vous ne pouvez pas accidentellement bousiller par exemple en manquant un else.

0
NiklasJ

La meilleure façon de réduire complexité cyclomatique dans les cas où vous avez beaucoup de if->then statements consiste à utiliser un dictionnaire ou une liste (en fonction de la langue) pour stocker la valeur de clé (si la valeur de l'instruction ou une valeur de), puis une valeur/résultat de la fonction.

Par exemple, au lieu de (C #):

if (i > 10) { return "Two"; }
else if (i > 8) { return "Four" }
else if (i > 4) { return "Eight" }
return "Ten";  //etc etc say anything after 3 or 4 values

Je peux simplement

var results = new Dictionary<int, string>
{
  { 10, "Two" },
  { 8, "Four"},
  { 4, "Eight"},
  { 0, "Ten"},
}

foreach(var key in results.Keys)
{
  if (i > results[key]) return results.Values[key];
}

Si vous utilisez des langages plus modernes, vous pouvez stocker plus de logique , puis simplement des valeurs (c #). Ce ne sont vraiment que des fonctions en ligne, mais vous pouvez également simplement pointer vers d'autres fonctions si la logique est de mettre en ligne narly.

var results = new Dictionary<Func<int, bool>, Func<int, string>>
{
  { (i) => return i > 10; ,
    (i) => return i.ToString() },
  // etc
};

foreach(var key in results.Keys)
{ 
  if (key(i)) return results.Values[key](i);
}
0
Erik Philips

J'essaie de suivre les suggestions de code propre d'oncle Bob et en particulier de garder les méthodes courtes.

Je ne parviens cependant pas à raccourcir cette logique:

if (checkCondition()) {addAlert(1);}
else if (checkCondition2()) {addAlert(2);}
else if (checkCondition3()) {addAlert(3);}
else if (checkCondition4()) {addAlert(4);}

Votre code est déjà trop court, mais la logique elle-même ne doit pas être modifiée. À première vue, il semble que vous vous répétiez avec quatre appels à checkCondition(), et il est seulement évident que chacun est différent après avoir relu le code attentivement. Vous devez ajouter les noms de format et de fonction appropriés, par exemple:

if (is_an_Apple()) {
  addAlert(1);
}
else if (is_a_banana()) {
  addAlert(2);
}
else if (is_a_cat()) {
  addAlert(3);
}
else if (is_a_dog()) {
  addAlert(4);
}

Votre code doit être lisible avant tout. Ayant lu plusieurs livres de l'oncle Bob, je crois que c'est le message qu'il essaie constamment de faire passer.

0
CJ Dennis

Plus de deux clauses "else" obligent le lecteur du code à parcourir toute la chaîne pour trouver celle qui l'intéresse. Utilisez une méthode telle que: void AlertUponCondition (Condition condition) {switch (condition) {case Condition.Con1: ... break; Condition Condition.Con2: ... pause; etc ...} Où "Condition" est une énumération appropriée. Si nécessaire, renvoyez un booléen ou une valeur. Appelez-le comme ceci: AlertOnCondition (GetCondition ());

Cela ne peut vraiment pas être plus simple, ET c'est plus rapide que la chaîne if-else une fois que vous dépassez quelques cas.

0
Jimmy T