web-dev-qa-db-fra.com

Évitez les méthodes trop complexes - Complexité cyclomatique

Je ne sais pas comment procéder pour réduire la complexité cyclomatique. Sonar rapporte 13 alors que 10 est attendu. Je suis sûr que rien de mal à laisser cette méthode car elle me met au défi de respecter la règle de Sonar. Toute réflexion serait grandement appréciée.

 public static long parseTimeValue(String sValue) {

    if (sValue == null) {
        return 0;
    }

    try {
        long millis;
        if (sValue.endsWith("S")) {
            millis = new ExtractSecond(sValue).invoke();
        } else if (sValue.endsWith("ms")) {
            millis = new ExtractMillisecond(sValue).invoke();
        } else if (sValue.endsWith("s")) {
            millis = new ExtractInSecond(sValue).invoke();
        } else if (sValue.endsWith("m")) {
            millis = new ExtractInMinute(sValue).invoke();
        } else if (sValue.endsWith("H") || sValue.endsWith("h")) {
            millis = new ExtractHour(sValue).invoke();
        } else if (sValue.endsWith("d")) {
            millis = new ExtractDay(sValue).invoke();
        } else if (sValue.endsWith("w")) {
            millis = new ExtractWeek(sValue).invoke();
        } else {
            millis = Long.parseLong(sValue);
        }

        return millis;

    } catch (NumberFormatException e) {
        LOGGER.warn("Number format exception", e);
    }

    return 0;
}

Toutes les méthodes ExtractXXX sont définies comme static classes internes. Par exemple, comme celui ci-dessous -

    private static class ExtractHour {
      private String sValue;

      public ExtractHour(String sValue) {
         this.sValue = sValue;
      }

      public long invoke() {
         long millis;
         millis = (long) (Double.parseDouble(sValue.substring(0, sValue.length() - 1)) * 60 * 60 * 1000);
         return millis;
     }
 }

MISE À JOUR 1

Je vais m'installer avec un mélange de suggestions ici pour satisfaire le gars Sonar. Certainement place pour des améliorations et simplification.

La goyave Function n'est qu'une cérémonie indésirable ici. Je voulais mettre à jour la question sur l'état actuel. Rien n'est définitif ici. Donnez votre avis s'il vous plaît ..

public class DurationParse {

private static final Logger LOGGER = LoggerFactory.getLogger(DurationParse.class);
private static final Map<String, Function<String, Long>> MULTIPLIERS;
private static final Pattern STRING_REGEX = Pattern.compile("^(\\d+)\\s*(\\w+)");

static {

    MULTIPLIERS = new HashMap<>(7);

    MULTIPLIERS.put("S", new Function<String, Long>() {
        @Nullable
        @Override
        public Long apply(@Nullable String input) {
            return new ExtractSecond(input).invoke();
        }
    });

    MULTIPLIERS.put("s", new Function<String, Long>() {
        @Nullable
        @Override
        public Long apply(@Nullable String input) {
            return new ExtractInSecond(input).invoke();
        }
    });

    MULTIPLIERS.put("ms", new Function<String, Long>() {
        @Nullable
        @Override
        public Long apply(@Nullable String input) {
            return new ExtractMillisecond(input).invoke();
        }
    });

    MULTIPLIERS.put("m", new Function<String, Long>() {
        @Nullable
        @Override
        public Long apply(@Nullable String input) {
            return new ExtractInMinute(input).invoke();
        }
    });

    MULTIPLIERS.put("H", new Function<String, Long>() {
        @Nullable
        @Override
        public Long apply(@Nullable String input) {
            return new ExtractHour(input).invoke();
        }
    });

    MULTIPLIERS.put("d", new Function<String, Long>() {
        @Nullable
        @Override
        public Long apply(@Nullable String input) {
            return new ExtractDay(input).invoke();
        }
    });

    MULTIPLIERS.put("w", new Function<String, Long>() {
        @Nullable
        @Override
        public Long apply(@Nullable String input) {
            return new ExtractWeek(input).invoke();
        }
    });

}

public static long parseTimeValue(String sValue) {

    if (isNullOrEmpty(sValue)) {
        return 0;
    }

    Matcher matcher = STRING_REGEX.matcher(sValue.trim());

    if (!matcher.matches()) {
        LOGGER.warn(String.format("%s is invalid duration, assuming 0ms", sValue));
        return 0;
    }

    if (MULTIPLIERS.get(matcher.group(2)) == null) {
        LOGGER.warn(String.format("%s is invalid configuration, assuming 0ms", sValue));
        return 0;
    }

    return MULTIPLIERS.get(matcher.group(2)).apply(matcher.group(1));
}

private static class ExtractSecond {
    private String sValue;

    public ExtractSecond(String sValue) {
        this.sValue = sValue;
    }

    public long invoke() {
        long millis;
        millis = Long.parseLong(sValue);
        return millis;
    }
}

private static class ExtractMillisecond {
    private String sValue;

    public ExtractMillisecond(String sValue) {
        this.sValue = sValue;
    }

    public long invoke() {
        long millis;
        millis = (long) (Double.parseDouble(sValue));
        return millis;
    }
}

private static class ExtractInSecond {
    private String sValue;

    public ExtractInSecond(String sValue) {
        this.sValue = sValue;
    }

    public long invoke() {
        long millis;
        millis = (long) (Double.parseDouble(sValue) * 1000);
        return millis;
    }
}

private static class ExtractInMinute {
    private String sValue;

    public ExtractInMinute(String sValue) {
        this.sValue = sValue;
    }

    public long invoke() {
        long millis;
        millis = (long) (Double.parseDouble(sValue) * 60 * 1000);
        return millis;
    }
}

private static class ExtractHour {
    private String sValue;

    public ExtractHour(String sValue) {
        this.sValue = sValue;
    }

    public long invoke() {
        long millis;
        millis = (long) (Double.parseDouble(sValue) * 60 * 60 * 1000);
        return millis;
    }
}

private static class ExtractDay {
    private String sValue;

    public ExtractDay(String sValue) {
        this.sValue = sValue;
    }

    public long invoke() {
        long millis;
        millis = (long) (Double.parseDouble(sValue) * 24 * 60 * 60 * 1000);
        return millis;
    }
}

private static class ExtractWeek {
    private String sValue;

    public ExtractWeek(String sValue) {
        this.sValue = sValue;
    }

    public long invoke() {
        long millis;
        millis = (long) (Double.parseDouble(sValue) * 7 * 24 * 60 * 60 * 1000);
        return millis;
    }
}

}


MISE À JOUR 2

Bien que j'aie ajouté ma mise à jour, cela ne vaut que le temps. Je vais continuer car Sonar ne se plaint plus. Ne vous inquiétez pas beaucoup et j'accepte la réponse de Mattnz car c'est la voie à suivre et je ne veux pas donner le mauvais exemple à ceux qui se heurtent à cette question. Bottom line - Ne pas trop ingénieur pour le bien de Sonar (ou Half Baked Project Manager) se plaint de CC. Faites juste ce qui vaut un sou pour le projet. Merci à tous.

23
asyncwait

Réponse en génie logiciel:

Ce n'est là qu'un des nombreux cas où le simple fait de compter les haricots simples à compter vous fera faire la mauvaise chose. Ce n'est pas une fonction complexe, ne la changez pas. La complexité cyclomatique n'est qu'un guide de la complexité, et vous l'utilisez mal si vous modifiez cette fonction en fonction de celle-ci. C'est simple, lisible, maintenable (pour l'instant), s'il grossit à l'avenir, le CC montera en flèche de façon exponentielle et il obtiendra l'attention dont il a besoin quand il en a besoin, pas avant.

Minion travaillant pour une grande multinationale Réponse:

Les organisations sont remplies d'équipes de compteurs de haricots surpayées et improductives. Garder les compteurs de haricots heureux est plus facile, et certainement plus sage, que de faire la bonne chose. Vous devez changer la routine pour ramener le CC à 10, mais soyez honnête sur la raison pour laquelle vous le faites - pour garder les compteurs de haricots hors de votre dos. Comme suggéré dans les commentaires - des "analyseurs monadiques" pourraient aider

45
mattnz

Merci à @JimmyHoffa, @MichaelT et @ GlenH7 pour leur aide!

Python

Tout d'abord, vous ne devriez vraiment accepter que les préfixes connus, c'est-à-dire "H" ou "h". Si vous avez pour accepter les deux, vous devez effectuer une opération pour la rendre cohérente afin d'économiser de la place sur votre carte.

Dans python vous pouvez créer un dictionnaire.

EXTRACTION_MAP = {
    'S': ExtractSecond,
    'ms': ExtractMillisecond,
    'm': ExtractMinute,
    'H': ExtractHour,
    'd': ExtractDay,
    'w': ExtractWeek
}

Ensuite, nous voulons que la méthode utilise ceci:

def parseTimeValue(sValue)
    ending = ''.join([i for i in sValue if not i.isdigit()])
    return EXTRACTION_MAP[ending](sValue).invoke()

Devrait avoir une meilleure complexité cyclomatique.


Java

Nous n'avons besoin que de 1 (un) de chaque multiplicateur. Permet de les mettre dans une carte comme l'ont suggéré d'autres réponses.

Map<String, Float> multipliers = new HashMap<String, Float>();
    map.put("S", 60 * 60);
    map.put("ms", 60 * 60 * 1000);
    map.put("m", 60);
    map.put("H", 1);
    map.put("d", 1.0 / 24);
    map.put("w", 1.0 / (24 * 7));

Ensuite, nous pouvons simplement utiliser la carte pour saisir le bon convertisseur

Pattern foo = Pattern.compile(".*(\\d+)\\s*(\\w+)");
Matcher bar = foo.matcher(sValue);
if(bar.matches()) {
    return (long) (Double.parseDouble(bar.group(1)) * multipliers.get(bar.group(2);
}
16
Ampt

Depuis que vous return millis à la fin de cette horrible ifelseifelse de toute façon, la première chose qui me vient à l'esprit est de renvoyer la valeur immédiatement à partir de if-blocks. Cette approche suit celle qui est répertoriée dans le catalogue des modèles de refactorisation sous la forme Remplacer le conditionnel imbriqué par des clauses de garde .

Une méthode a un comportement conditionnel qui ne précise pas quel est le chemin normal d'exécution

Utilisez des clauses de garde pour tous les cas spéciaux

Cela vous aidera à vous débarrasser des autres, à aplatir le code et à rendre Sonar heureux:

    if (sValue.endsWith("S")) {
        return new ExtractSecond(sValue).invoke();
    } // no need in else after return, code flattened

    if (sValue.endsWith("ms")) {
        return new ExtractMillisecond(sValue).invoke();
    }

    // and so on...
    return Long.parseLong(sValue); // forget millis, these aren't needed anymore

Une autre chose à considérer est de supprimer le bloc try-catch. Cela réduira également la complexité cyclomatique, mais la principale raison pour laquelle je recommande c'est qu'avec ce bloc, il n'y ait aucun moyen pour le code de l'appelant de distinguer légalement l'analyse 0 de l'exception de format numérique.

À moins que vous ne soyez sûr à 200% que le retour de 0 pour les erreurs d'analyse est ce dont le code de l'appelant a besoin, vous feriez mieux de propager cette exception et de laisser le code de l'appelant décider comment la traiter. Il est généralement plus pratique de décider à l'appelant s'il faut annuler l'exécution ou réessayer d'obtenir l'entrée, ou revenir à une valeur par défaut comme 0 ou -1 ou autre.


Votre extrait de code pour un exemple ExtractHour me fait sentir que la fonctionnalité ExtractXXX est conçue d'une manière loin d'être optimale. Je parie sans réfléchir toutes les classes restantes répète les mêmes parseDouble et substring, et en multipliant des trucs comme 60 et 1000 encore et encore et encore.

C'est parce que vous avez manqué le essence de ce qui doit être fait en fonction de sValue - à savoir, il définit combien de caractères à couper à la fin de la chaîne et ce qui serait la valeur du multiplicateur. Si vous concevez votre objet "central" autour de ces fonctionnalités essentielles, il ressemblerait à ceci:

private static class ParseHelper {
    // three things you need to know to parse:
    final String source;
    final int charsToCutAtEnd;
    final long multiplier;

    ParseHelper(String source, int charsToCutAtEnd, long multiplier) {
        this.source = source == null ? "0" : source; // let's handle null here
        this.charsToCutAtEnd = charsToCutAtEnd;
        this.multiplier = multiplier;
    }

    long invoke() {
        // NOTE consider Long.parseLong instead of Double.parseDouble here
        return (long) (Double.parseDouble(cutAtEnd()) * multiplier);
    }

    private String cutAtEnd() {
        if (charsToCutAtEnd == 0) {
            return source;
        }
        // write code that cuts 'charsToCutAtEnd' from the end of the 'source'
        throw new UnsupportedOperationException();
    }
}

Après cela, vous auriez besoin d'un code qui configure les objets ci-dessus par condition particulière si elle est remplie, ou "contourne" dans le cas contraire. Cela pourrait se faire comme suit:

private ParseHelper setupIfInSecond(ParseHelper original) {
    final String sValue = original.source;
    return sValue.endsWith("s") && !sValue.endsWith("ms")
            ? new ParseHelper(sValue, 1, 1000)
            :  original; // bypass
}

private ParseHelper setupIfMillisecond(ParseHelper original) {
    final String sValue = original.source;
    return sValue.endsWith("ms")
            ? new ParseHelper(sValue, 2, 1)
            : original; // bypass
}

// and so on...

Basé sur ci-dessus blocs de construction, le code de votre méthode pourrait ressembler à ceci:

public long parseTimeValue(String sValue) {

   return setupIfSecond(
           setupIfMillisecond(
           setupIfInSecond(
           setupIfInMinute(
           setupIfHour(
           setupIfDay(
           setupIfWeek(
           new ParseHelper(sValue, 0, 1))))))))
           .invoke();
}

Vous voyez, il n'y a plus de complexité, pas d'accolades à l'intérieur de la méthode (ni retours multiples comme dans ma suggestion initiale de force brute sur l'aplatissement du code). Il vous suffit de vérifier séquentiellement l'entrée et d'ajuster le traitement selon vos besoins.

4
gnat

Si vous voulez vraiment le refactoriser, vous pouvez faire quelque chose comme ceci:

// All of your Extract... classes will have to implement this interface!
public Interface TimeExtractor
{
    public long invoke();
}

private static class ExtractHour implements TimeExtractor
{
  private String sValue;


  /*Not sure what this was for - might not be necessary now
  public ExtractHour(String sValue)
  {
     this.sValue = sValue;
  }*/

  public long invoke(String s)
  {
     this.sValue = s;
     long millis;
     millis = (long) (Double.parseDouble(sValue.substring(0, sValue.length() - 1)) * 60 * 60 * 1000);
     return millis;
 }
}

private static HashMap<String, TimeExtractor> extractorMap= new HashMap<String, TimeExtractor>();

private void someInitMethod()
{
   ExtractHour eh = new ExtractorHour;
   extractorMap.add("H",eh);
   /*repeat for all extractors */
}

public static long parseTimeValue(String sValue)
{
    if (sValue == null)
    {
        return 0;
    }
    String key = extractKeyFromSValue(sValue);
    long millis;
    TimeExtractor extractor = extractorMap.get(key);
    if (extractor!=null)
    {
      try
      {
         millis= extractor.invoke(sValue);
      }
        catch (NumberFormatException e)
      {
          LOGGER.warn("Number format exception", e);
      }
    }
    else
       LOGGER.error("NO EXTRACTOR FOUND FOR "+key+", with sValue: "+sValue);

    return millis;
}

L'idée est que vous ayez une carte de clés (ce que vous utilisez dans "se termine avec" tout le temps) qui correspondent à des objets spécifiques qui effectuent le traitement que vous souhaitez.

C'est un peu rude ici mais j'espère que c'est assez clair. Je n'ai pas rempli les détails de extractKeyFromSValue() parce que je ne connais pas suffisamment ces chaînes pour le faire correctement. Il semble que ce soit les 1 ou 2 derniers caractères non numériques (un regex pourrait probablement l'extraire assez facilement, peut-être que .*([a-zA-Z]{1,2})$ fonctionnerait), mais je ne suis pas sûr à 100% ...


Réponse originale:

Tu pourrais changer

else if (sValue.endsWith("H") || sValue.endsWith("h")) {

à

else if (sValue.toUpper().endsWith("H")) {

Cela pourrait vous faire économiser un peu, mais honnêtement, je ne m'en inquiéterais pas trop. Je suis d'accord avec vous que je ne pense pas qu'il y ait beaucoup de mal à laisser la méthode telle qu'elle est. Au lieu d'essayer "d'obéir aux règles du Sonar", essayez de "rester proche des directives du Sonar, autant qu'il est raisonnablement possible".

Vous pouvez vous rendre fou en essayant de suivre toutes les règles que ces outils d'analyse contiennent, mais vous devez également décider si les règles ont du sens pour votre projet et pour des cas spécifiques où le temps passé à la refactorisation pourrait ne pas en valoir la peine .

Pour être honnête, toutes les réponses techniques ci-dessus semblent terriblement compliquées pour la tâche à accomplir. Comme cela a déjà été écrit, le code lui-même est propre et bon, donc j'opterais pour le plus petit changement possible pour satisfaire le compteur de complexité. Que diriez-vous de la refactorisation suivante:

public static long parseTimeValue(String sValue) {

    if (sValue == null) {
        return 0;
    }

    try {
        return getMillis(sValue);
    } catch (NumberFormatException e) {
        LOGGER.warn("Number format exception", e);
    }

    return 0;
}

private static long getMillis(String sValue) {
    if (sValue.endsWith("S")) {
        return new ExtractSecond(sValue).invoke();
    } else if (sValue.endsWith("ms")) {
        return new ExtractMillisecond(sValue).invoke();
    } else if (sValue.endsWith("s")) {
        return new ExtractInSecond(sValue).invoke();
    } else if (sValue.endsWith("m")) {
        return new ExtractInMinute(sValue).invoke();
    } else if (sValue.endsWith("H") || sValue.endsWith("h")) {
        return new ExtractHour(sValue).invoke();
    } else if (sValue.endsWith("d")) {
        return new ExtractDay(sValue).invoke();
    } else if (sValue.endsWith("w")) {
        return new ExtractWeek(sValue).invoke();
    } else {
        return Long.parseLong(sValue);
    }
}

Si je compte correctement, la fonction extraite devrait avoir une complexité de 9, ce qui répond toujours aux exigences. Et c'est essentiellement le même code qu'avant, ce qui est une bonne chose, car le code était bon au début.

De plus, les lecteurs de Clean Code pourraient apprécier le fait que la méthode de niveau supérieur est maintenant simple et courte, tandis que celle extraite traite des détails.

0
Arides

Relatif à votre commentaire de:

Bottom line - Ne pas trop ingénieur pour le bien de Sonar (ou Half Baked Project Manager) se plaint de CC. Faites juste ce qui vaut un sou pour le projet.

Une autre option à considérer est de modifier les normes de codage de votre équipe pour des situations comme celle-ci. Vous pouvez peut-être ajouter une sorte de vote d'équipe pour fournir une mesure de la gouvernance et éviter les situations de raccourci.

Mais changer les normes de l'équipe en réponse à des situations qui n'ont pas de sens est le signe d'une bonne équipe avec la bonne attitude à l'égard des normes. Les normes sont là pour aider l'équipe, pas pour gêner l'écriture de code.

0
user53019

Vous pouvez envisager d'utiliser une énumération pour stocker tous vos cas et prédicats disponibles pour les valeurs correspondantes. Comme mentionné précédemment, votre fonction est suffisamment lisible pour ne pas la modifier. Ces mesures sont là pour vous aider et non l'inverse.

//utility class for matching values
private static class ValueMatchingPredicate implements Predicate<String>{
    private final String[] suffixes;

    public ValueMatchingPredicate(String[] suffixes) {      
        this.suffixes = suffixes;
    }

    public boolean apply(String sValue) {
        if(sValue == null) return false;

        for (String suffix : suffixes) {
            if(sValue.endsWith(suffix)) return true;
        }

        return false;
    }

    public static Predicate<String> withSuffix(String... suffixes){         
        return new ValueMatchingPredicate(suffixes);
    }       
}

//enum containing all possible options
private static enum TimeValueExtractor {                
    SECOND(
        ValueMatchingPredicate.withSuffix("S"), 
        new Function<String, Long>(){ 
            public Long apply(String sValue) {  return new ExtractSecond(sValue).invoke(); }
        }),

    MILISECOND(
        ValueMatchingPredicate.withSuffix("ms"), 
        new Function<String, Long>(){
            public Long apply(String sValue) { return new ExtractMillisecond(sValue).invoke(); }
        }),

    IN_SECOND(
        ValueMatchingPredicate.withSuffix("s"),
        new Function<String, Long>(){
            public Long apply(String sValue) { return new ExtractInSecond(sValue).invoke(); }
        }),

    IN_MINUTE(
        ValueMatchingPredicate.withSuffix("m"),
        new Function<String, Long>(){
            public Long apply(String sValue) {  return new ExtractInMinute(sValue).invoke(); }
        }),

    HOUR(
        ValueMatchingPredicate.withSuffix("H", "h"),
        new Function<String, Long>(){
            public Long apply(String sValue) {  return new ExtractHour(sValue).invoke(); }
        }),

    DAY(
        ValueMatchingPredicate.withSuffix("d"),
        new Function<String, Long>(){
            public Long apply(String sValue) {  return new ExtractDay(sValue).invoke(); }
        }),

    WEEK(
        ValueMatchingPredicate.withSuffix("w"),
        new Function<String, Long>(){
            public Long apply(String sValue) {  return new ExtractWeek(sValue).invoke(); }
        });

    private final Predicate<String>      valueMatchingRule;
    private final Function<String, Long> extractorFunction;

    public static Long DEFAULT_VALUE = 0L;

    private TimeValueExtractor(Predicate<String> valueMatchingRule, Function<String, Long> extractorFunction) {
        this.valueMatchingRule = valueMatchingRule;
        this.extractorFunction = extractorFunction;
    }

    public boolean matchesValueSuffix(String sValue){
        return this.valueMatchingRule.apply(sValue);
    }

    public Long extractTimeValue(String sValue){
        return this.extractorFunction.apply(sValue);
    }

    public static Long extract(String sValue) throws NumberFormatException{
        TimeValueExtractor[] extractors = TimeValueExtractor.values();

        for (TimeValueExtractor timeValueExtractor : extractors) {
            if(timeValueExtractor.matchesValueSuffix(sValue)){
                return timeValueExtractor.extractTimeValue(sValue);
            }
        }

        return DEFAULT_VALUE;
    }
}

//your function
public static long parseTimeValue(String sValue){
    try{
        return TimeValueExtractor.extract(sValue);
    } catch (NumberFormatException e) {
        //LOGGER.warn("Number format exception", e);
        return TimeValueExtractor.DEFAULT_VALUE;
    }
}
0
Gwozdziu