避免过于复杂的方法-循环复杂性


23

不确定如何使用这种方法来降低环复杂性。声纳报告为13,而预期为10。我敢肯定,保持这种方法不会造成任何危害,不过,这只是挑战我如何遵循Sonar的法则。任何想法将不胜感激。

 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的人满意。绝对有改进和简化的空间。

番石榴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项目经理)而抱怨工程师。只要做一个值得一分钱的项目。谢谢大家。


4
OO最简单的临时答案:private static Dictionary<string,Func<string,long>> _mappingStringToParser;我将把剩下的作为练习留给您(或现在比我有更多空闲时间的人)。还有一个更简洁的API,如果你有一元解析器任何熟悉被发现,但现在我不会去那里..
吉米·霍法

如果您可以在“ monadic解析器”上花些时间,以及如何将其应用于像这样的很小的函数,将不胜感激。并且,这段代码来自Java。
asyncwait

如何ExtractBlah定义类?这些是来自图书馆还是自制的?
蚊蚋

4
附加的代码使我更进一步地实现了一个简单的实现:您的实际差异是乘数。创建一个这样的映射:从sValue的末尾拉出字母字符,使用这些字符作为键,然后将所有字符拉到从正面开始的Alpha值中,以该值乘以映射的乘法器。
Jimmy Hoffa 2013年

2
重新更新:我是唯一一个“工程过度”的人吗?
mattnz

Answers:


46

软件工程答案:

这只是许多简单计数的豆会使您做错事情的众多情况之一。它不是一个复杂的功能,请不要更改它。循环复杂度仅是复杂度的指南,如果您基于此来更改此功能,则使用率很低。它的简单性,可读性,可维护性(目前),如果将来变得更大,CC将成倍增长,并在需要时(而不是以前)得到需要的关注。

奴才为大型跨国公司工作答案:

组织中充斥着高价,无用的bean柜台团队。使豆计数器保持满意比做正确的事更容易,当然也更明智。您需要更改例程以将CC降低到10,但是要诚实地说明为什么这样做-不要让Bean计数器掉下来。如评论中所建议-“ monadic解析器”可能会有所帮助


14
+1是为了尊重规则的原因,而不是规则本身
Radu Murzea

5
说得好!但是,在其他情况下,如果您的CC = 13具有多个嵌套级别和复杂的(分支)逻辑,则最好至少尝试简化它。
grizwako

16

感谢@ JimmyHoffa,@ MichaelT和@ GlenH7的帮助!

蟒蛇

首先,您实际上应该只接受已知的前缀,即“ H”或“ h”。如果必须接受两者,则应执行一些操作以使其一致以节省地图上的空间。

在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()

应该有更好的圈复杂度。


爪哇

每个乘数只需要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);
}

是的,将字符串映射到转换因子将是一个更简单的解决方案。如果他们不需要这些对象,那么就应该摆脱它们,但是我看不到程序的其余部分,因此也许他们将这些对象更多地用作其他地方的对象...?
FrustratedWithFormsDesigner

他们可以使用@FrustratedWithFormsDesigner,但是在该方法的范围内,它仅返回一个long且实例化的对象不在范围之内。顺便说一句,这具有副作用,如果更频繁地调用此代码,则会减少无状态的频繁使用的短暂对象的数量。

这些答案都不能依靠与可能无效的假设来提供与原始程序相同的解决方案。Java代码:您如何确定方法唯一要做的就是应用乘法器?Python代码:如何确定该字符串不允许包含数字以外的前导(或中点)字符(例如“ 123.456s”)。
mattnz

@mattnz-再次查看提供的示例中的变量名称。很明显,OP正在接收一个时间单位作为字符串,然后需要将其转换为另一种时间单位。因此,此答案中提供的示例直接与OP的领域有关。忽略这一方面,答案仍然提供了可用于其他领域的通用方法。此答案解决了所提出的问题,而不是可能已经提出的问题。

5
@mattnz-1)OP从未在其示例中指定该内容,因此可能不在乎。您怎么知道这些假设是无效的?2)通用方法仍然可以使用,可能需要更复杂的正则表达式。3)答案的重点是提供解决环复杂性的概念性途径,而不一定是一个具体的可编译答案。4)尽管此答案忽略了“做复杂性事项”的广义问题,但它通过显示代码的替代形式来间接回答问题。

3

由于无论如何您都return millis处于糟糕的ifelseifelse末尾,所以想到的第一件事就是立即从if块内返回值。此方法遵循重构模式目录中列出的一种方法,即“ 使用Guard子句替换嵌套条件”

方法具有条件行为,无法明确执行的正常路径是什么

在所有特殊情况下使用警卫条款

它将帮助您摆脱其他问题,简化代码并使Sonar满意:

    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

值得考虑的另一件事是删除try-catch块。这也将降低循环复杂度,但是我建议使用此块的主要原因是,调用方代码无法将合法解析的0与数字格式异常区分开。

除非您200%确信调用者代码需要解析错误返回0,否则您最好将异常传播出去,然后让调用者代码决定如何处理它。通常,决定调用方是中止执行还是重试获取输入,还是退回到某个默认值(如0或-1或其他),会更加方便。


您的示例ExtractHour的代码片段使我觉得ExtractXXX功能的设计远非最佳。我敢打赌,其余的每个类都反复地重复相同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();
}

您会看到,这里没有任何复杂性,方法内部根本没有花括号(也没有多重回报,例如我对拼合代码的原始蛮力建议)。您只需顺序检查输入并根据需要调整处理即可。


1

如果您确实要重构它,则可以执行以下操作:

// 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")) {

这可能会为您节省一些时间,但是说实话,我不会对此太担心。我同意您的看法,我认为将方法保持原样不会有多大危害。不要试图“服从声纳的规则”,而要“尽可能合理地保持接近声纳的准则”。

您可能会疯狂地尝试遵循这些分析工具所包含的每条规则,但是您还必须决定这些规则是否对您的项目有意义,以及在某些特定情况下,花在重构上的时间可能不值得。


3
声纳并没有太大用处。我尝试这种方式很有趣,至少可以学习一两个。代码已移至暂存。
2013年

@asyncwait:啊,我想您比那时候更重视声纳的这份报告。是的,我建议的更改不会巨大的差异-我不认为它会带你从13到10,但在一些工具,我已经看到了这样的事情做一个有点明显的区别。
FrustratedWithFormsDesigner

只需添加另一个IF分支,CC就会增加到+1。
2013年

0

您可能会考虑使用一个枚举来存储所有可用的案例和谓词以匹配值。如前所述,您的函数具有足够的可读性,只是使其保持不变。这些指标可以帮助您,反之亦然。

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

与您的评论有关:

底线-不要为了Sonar(或Half Baked项目经理)而抱怨工程师。只要做一个值得一分钱的项目。

要考虑的另一种选择是针对这种情况更改团队的编码标准。也许您可以添加某种形式的团队投票,以提供某种程度的治理并避免捷径。

但是,在无意义的情况下更改团队的标准是一个好的团队对标准持正确态度的标志。这些标准可以帮助团队,而不是妨碍编写代码。


0

坦白地说,上述所有技术响应对于手头的任务而言似乎都非常复杂。就像已经写过的那样,代码本身是干净且良好的,因此我将选择尽可能小的更改以满足复杂性计数器。如何进行以下重构:

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的读者可能会喜欢这样的事实,即顶层方法现在既简单又简短,而摘录的方法则涉及细节。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.