web-dev-qa-db-ja.com

あまりにも複雑な方法を避ける-循環的複雑度

Cyclomatic Complexityを削減するためにこの方法をどのように進めるかわかりません。ソナーは13を報告しますが、10が予想されます。この方法をそのままにしておいても害はないと確信していますが、ソナーのルールに従う方法に挑戦するだけです。どんな考えでも大歓迎です。

 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;
}

すべてのExtractXXXメソッドは、static内部クラスとして定義されています。たとえば、以下のように-

    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;
     }
 }

アップデート1

ここでは、Sonarの人を満足させるために、さまざまな提案をまとめて解決します。間違いなく、改善と簡素化の余地があります。

Guava Functionは、ここでは不要な式です。現在の状況に関する質問を更新したかった。ここでは最終的なものはありません。あなたの考えを注いでください。

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;
    }
}

}


アップデート2

アップデートを追加しましたが、それだけの時間はあります。 Sonarが不満を出さなくなったので、次に進みます。あまり心配しないでください。私はmattnzの回答を受け入れます。これは方法であり、この質問にぶつかった人に悪い例を示したくないからです。結論-Sonar(またはHalf Baked Project Manager)がCCについて不平を言うために、やりすぎないでください。プロジェクトにペニーの価値があることをしてください。ありがとうございます。

23
asyncwait

ソフトウェアエンジニアリングの回答:

これは、カウントが簡単なBeanを数えるだけで間違ったことが行われる多くのケースの1つにすぎません。複雑な機能ではありません。変更しないでください。循環的複雑度は単に複雑度へのガイドであり、それに基づいてこの関数を変更すると、それをうまく使用できなくなります。そのシンプルで、読みやすく、保守可能な(現時点では)将来的に大きくなると、CCは指数関数的に急上昇し、以前ではなく必要なときに必要な注意を引くようになります。

大規模な多国籍企業で働いているミニオン回答:

組織は、過剰に支払われた非生産的な豆売り場のチームでいっぱいです。豆のカウンターを幸せに保つことは、正しいことをするよりも簡単で確実です。 CCを10に下げるには、ルーチンを変更する必要がありますが、それを行う理由について正直に言ってください。Beanのカウンターを背中に寄せないようにするためです。コメントで示唆されているように-「モナディックパーサー」が役立つかもしれません

45
mattnz

@ JimmyHoffa、@ MichaelT、@ GlenH7の協力に感謝します。

Python

まず最初に、「H」または「h」のどちらかである既知のプレフィックスのみを実際に受け入れる必要があります。 haveで両方を受け入れる場合は、マップのスペースを節約するために、いくつかの操作を実行して一貫性を保つ必要があります。

pythonでは、辞書を作成できます。

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

次に、メソッドでこれを使用する必要があります。

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

サイクロマティックの複雑度が高くなるはずです。


Java

各乗数は1つだけです。他のいくつかの答えが示唆しているように、それらを地図に入れましょう。

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));

次に、マップを使用して適切なコンバーターを取得します

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

あなたからreturn millisとにかくひどいifelseifelseの最後に、最初に頭に浮かぶのは、ifブロック内からすぐに値を返すことです。このアプローチは、リファクタリングパターンのカタログに ネストされた条件付きをガード句で置き換える としてリストされているアプローチに従います。

メソッドには条件付きの動作があり、実行の通常のパスが何であるかが明確にされていません

すべての特殊なケースにガード句を使用します

それはあなたが他のものを取り除くのを助け、コードを平らにし、ソナーを幸せにします:

    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

検討する価値のあるもう1つのことは、try-catchブロックを削除することです。これにより、循環的複雑度も低下しますが、このブロックを使用することをお勧めする主な理由は、呼び出し元コードが法的に解析された0と数値形式の例外を区別する方法がないためです。

解析エラーに対して0を返すことが呼び出し元コードに必要であると200%確信していない限り、その例外を上に伝播し、呼び出し元コードに対処方法を決定させることができます。通常、実行を中止するか、入力の取得を再試行するか、0や-1などのデフォルト値にフォールバックするかを呼び出し側で決定する方が便利です。


ExtractHourの例のコードスニペットは、ExtractXXX機能が最適とはかけ離れた方法で設計されていると感じさせます。私は残りのクラスすべてを不注意に賭けます repeats 同じparseDoublesubstring、そして60と1000のようなものを何度も繰り返します。

これは、sValueに応じて実行する必要があるエッセンスを逃したためです。つまり、文字列の末尾から切り取る文字数と、乗数の値はどうなるでしょう。これらの重要な機能を中心に「コア」オブジェクトを設計すると、次のようになります。

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();
    }
}

その後、条件を満たす場合は上記のオブジェクトを特定の条件ごとに設定するか、そうでなければ「バイパス」するコードが必要になります。これは、次のように行うことができます。

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...

上記ビルディングブロックに基づいて、メソッドのコードは次のようになります。

public long parseTimeValue(String sValue) {

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

ご覧のとおり、複雑さは残っておらず、メソッド内に中括弧はありません(または、コードのフラット化に関する元のブルートフォースの提案のように 複数の戻り値 もありません)。入力を順次確認し、必要に応じて処理を調整するだけです。

4
gnat

本当にそれをリファクタリングしたいなら、あなたはこのようなことをすることができます:

_// 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;
}
_

アイデアは、必要な処理を行う特定のオブジェクトにマップするキーのマップ( "endsWith"で常に使用するもの)を持っていることです。

ここでは少し荒いですが、十分に明確であることを願っています。 extractKeyFromSValue()の詳細を入力しなかったのは、これらの文字列が適切に機能するために必要な文字列がわからないためです。それは最後の1つまたは2つの非数値文字のようです(正規表現はおそらくそれを簡単に抽出でき、おそらく.*([a-zA-Z]{1,2})$が機能するでしょう)が、私は100%確信がありません...


元の答え:

あなたは変えることができます

_else if (sValue.endsWith("H") || sValue.endsWith("h")) {
_

_else if (sValue.toUpper().endsWith("H")) {
_

それはあなたを少し救うかもしれませんが、正直なところ、私はそれについてあまり心配しません。この方法をそのままにしてもそれほど害はないと私はあなたに同意します。 「ソナーのルールに従う」のではなく、「合理的に可能な限りソナーのガイドラインに近づく」ようにしてください。

これらの分析ツールが持つすべてのルールに従うことを自分で狂わせることができますが、ルールがプロジェクトに意味があるかどうか、およびリファクタリングに費やされた時間がそれだけの価値がない特定のケースについても決定する必要があります。

正直なところ、上記のすべての技術的対応は、当面の作業にとって非常に複雑に見えます。すでに記述したように、コード自体はクリーンで優れているため、複雑さのカウンターを満たすために、可能な限り最小の変更を選択します。次のリファクタリングはどうですか:

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);
    }
}

私が正しく数えている場合、抽出された関数は9の複雑さを持つはずですが、それでも要件は満たされています。そして、それは基本的に以前と同じコードです。これは、コードが最初から良かったので、良いことです。

さらに、Clean Codeの読者は、トップレベルのメソッドがシンプルで短いという事実を享受しているかもしれませんが、抽出されたメソッドは詳細を扱っています

0
Arides

あなたのコメントに関連して:

結論-Sonar(またはHalf Baked Project Manager)がCCについて不平を言うために、やりすぎないでください。プロジェクトにペニーの価値があることをしてください。

考慮すべき別のオプションは、このような状況に合わせてチームのコーディング標準を変更することです。おそらく、ある種のチーム投票を追加して、ガバナンスの指標を提供し、ショートカットの状況を回避できます。

しかし、意味をなさない状況に応じてチームの標準を変更することは、標準について正しい態度を持つ優れたチームのしるしです。標準は、コードの記述の邪魔にならず、チームを支援するためにあります。

0
user53019

利用可能なすべてのケースと一致する値の述語を格納するために列挙型を使用することを検討する場合があります。前に述べたように、関数はそのままにしておくのに十分なほど読みやすくなっています。これらのメトリックは、逆ではないために役立ちます。

//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