在循环中设置标记以供以后使用是否有代码味道?


30

我有一段代码在其中迭代地图,直到某个条件为真,然后再使用该条件做更多的事情。

例:

Map<BigInteger, List<String>> map = handler.getMap();

if(map != null && !map.isEmpty())
{
    for (Map.Entry<BigInteger, List<String>> entry : map.entrySet())
    {
        fillUpList();

        if(list.size() > limit)
        {
            limitFlag = true;
            break;
        }
    }
}
else
{
    logger.info("\n>>>>> \n\t 6.1 NO entries to iterate over (for given FC and target) \n");
}

if(!limitFlag) // Continue only if limitFlag is not set
{
    // Do something
}

我觉得设置一个标志,然后用它做更多的事情是代码的味道。

我对吗?我该如何删除?


10
为什么感觉到有代码气味?在执行此操作时,您会预见到什么样的特定问题,而这些问题不会在其他结构下发生?
Ben Cottrell

13
@ gnasher729出于好奇,您会改用哪个术语?
Ben Cottrell

11
-1,您的示例没有任何意义。entry在函数循环内无处使用,我们只能猜测是什么list。是fillUpList为了填补list?为什么不将其作为参数?
布朗

13
我会重新考虑您对空格和空行的使用。
Daniel Jour

11
没有代码异味。“代码气味”是软件开发人员发明的一个术语,当他们看到不符合其精英标准的代码时,他们想hold之以鼻。
罗伯特·哈维

Answers:


70

使用布尔值实现其预期目的没有什么不对:记录二进制差异。

如果告诉我重构此代码,则可能会将循环放入其自己的方法中,以便赋值+ break变为return;。那么您甚至不需要变量,您可以简单地说

if(fill_list_from_map()) {
  ...

6
实际上,他的代码中的气味是long函数,需要将其拆分为较小的函数。您的建议是要走的路。
伯恩哈德·希勒

2
描述该代码第一部分的有用功能的一个更好的短语是查找在从那些映射项中累积一些东西后是否会超过该限制。我们还可以放心地假设这fillUpList()是一些代码(OP决定不共享),它实际上使用entry了迭代中的值。如果没有这个假设,则看起来循环主体没有使用循环迭代中的任何内容。
rwong

4
@Kilian:我只关心一个问题。此方法将填充列表,并将返回一个布尔值,该布尔值表示列表大小是否超出限制,因此名称“ fill_list_from_map”无法明确说明返回的布尔值代表什么(填充失败,上限等)。由于返回的布尔值是用于特殊情况的,因此从函数名称中看不出来。任何意见 ?PS:我们也可以考虑将命令查询分离。
Siddharth Trikha '18

2
@SiddharthTrikha你是对的,当我建议那条线时,我也有同样的担忧。但是尚不清楚该代码应填充到哪个列表中。如果列表始终相同,则不需要该标志,之后只需检查其长度即可。如果您确实需要知道是否有任何单独的填充量超出了限制,则必须以某种方式将这些信息传输到外部,而IMO的命令/查询分离原理并不是拒绝明显方法的充分理由:通过返回值。
Kilian Foth,

6
Bob叔叔在Clean Code的第45页上说:“函数应该做某事或回答某件事,但不能两者都做。要么您的函数应该更改对象的状态,要么应该返回有关该对象的某些信息。两者都经常导致混乱。”
CJ丹尼斯

25

它不一定是坏的,有时它是最好的解决方案。但是,在嵌套块设置标志这样可以使代码很难跟随。

问题是您有用于界定范围的块,但是您却拥有跨范围进行通信的标志,从而破坏了这些块的逻辑隔离。举例来说,limitFlag将是错误的,如果mapnull,那么如果“做一些事情” -code将被执行mapnull。这可能是您想要的,但可能是一个容易遗漏的错误,因为此标志的条件是在嵌套范围内的其他位置定义的。如果您可以将信息和逻辑保持在尽可能小的范围内,则应尝试这样做。


2
这就是我觉得这是代码异味的原因,因为这些块没有完全隔离,以后很难跟踪。所以我猜@Kilian的答案中的代码最接近我们可以得到的?
Siddharth Trikha '18

1
@SiddharthTrikha:很难说,因为我不知道代码实际上应该做什么。如果您只想检查地图是否包含至少一个列表大于限制的项目,我认为您可以使用一个anyMatch表达式来完成。
JacquesB

2
@SiddharthTrikha:可以通过将初始测试更改为guard子句来轻松解决范围问题if(map==null || map.isEmpty()) { logger.info(); return;},但这仅在我们看到的代码是函数的完整主体时才起作用,并且// Do something在映射的情况下不需要该部分为null或为空。
布朗

14

我建议不要对“代码气味”进行推理。这只是使自己的偏见合理化的最懒惰的方法。随着时间的流逝,您会产生很多偏见,很多偏见都是合理的,但是其中很多都是愚蠢的。

取而代之的是,您应该有实际的(即不是教条主义的)理由要胜于另一件事,并且要避免对所有类似的问题都拥有相同的答案。

“代码气味”适用于您思考的情况。如果您真的要考虑代码,那就做对吧!

在这种情况下,取决于周围的代码,决定实际上可以采取任何一种方式。这实际上取决于您认为是思考代码正在执行的最清晰方法。(“干净的”代码是向其他开发人员清楚地传达其所做的事情的代码,并使他们易于验证它是否正确)

很多时候,人们会编写分阶段构造的方法,其中代码将首先确定需要了解的数据,然后对数据进行操作。如果“确定”部分和“对其执行操作”部分都有些复杂,则这样做很有意义,并且通常可以在布尔标志的各个阶段之间携带“需要知道的内容”。不过,我真的希望您给旗帜起一个更好的名字。像“ largeEntryExists”之类的东西会使代码更简洁。

另一方面,如果“ //做某事”代码非常简单,则将其放在if块中而不是设置标志会更有意义。这使结果更接近原因,并且读者不必扫描其余代码即可确保该标志保留您将要设置的值。


5

是的,这是一种代码味道(提示进行此操作的所有人都拒绝投票)。

对我而言,关键是使用break语句。如果您不使用它,那么您将遍历比所需更多的项目,但是使用它会从循环中提供两个可能的退出点。

您的示例不是主要问题,但是您可以想象,随着循环内的一个或多个条件变得更加复杂,或者初始列表的顺序变得很重要,那么错误就会更容易爬入代码中。

当代码像您的示例一样简单时,可以简化为while循环或等效映射,过滤器构造。

当代码足够复杂以至于需要标记和中断时,将很容易出现错误。

因此,与所有代码气味一样:如果看到标志,请尝试将其替换为while。如果不能,请添加额外的单元测试。


向我+1。这肯定是一种代码气味,您可以清楚地说明原因以及如何处理它。
David Arno

@Ewan:SO as with all code smells: If you see a flag, try to replace it with a while您能举例说明一下吗?
Siddharth Trikha,

2
具有与环路多个出口可能使其更难原因有关,但在这种情况下,这样会重构它,使循环条件取决于标志-它会意味着更换for (Map.Entry<BigInteger, List<String>> entry : map.entrySet())for (Iterator<Map.Entry<BigInteger, List<String>>> iterator = map.entrySet().iterator(); iterator.hasNext() && !limitFlag; Map.Entry<BigInteger, List<String>> entry = iterator.next())。这是一个非常罕见的模式,与一个相对简单的中断相比,我在理解它时会遇到更多麻烦。
James_pic

@James_pic我的Java有点生疏,但是如果我们使用地图,那么我将使用收集器来汇总项目数量,并过滤掉超出限制的项目。但是,正如我所说的示例“还不错”,代码气味是一条警告您潜在问题的一般规则。这不是神圣的法律,您必须始终遵守
Ewan

1
您不是说“提示”而不是“排队”吗?
psmears '18

0

只需使用除limitFlag之外的其他名称即可告诉您实际要检查的内容。以及为什么地图不存在或为空时您要记录什么?limtFlag将是错误的,所有您关心的。如果映射为空,则循环很好,因此无需检查。


0

我认为,设置布尔值来传达您已经拥有的信息是不好的做法。如果没有简单的选择,那么可能表明存在更大的问题,例如封装不良。

您应该将for循环逻辑移到fillUpList方法中,以便在达到限制时中断逻辑。然后,直接检查列表的大小。

如果这破坏了您的代码,为什么呢?


0

首先是一般情况:使用标志来检查集合中的某些元素是否满足特定条件并不罕见。但是,我最常看到的解决此问题的模式是将支票移到其他方法中,然后直接从中返回(例如Kilian Foth在他的回答中所述):

private <T> boolean checkCollection(Collection<T> collection)
{
    for (T element : collection)
        if (checkElement(element))
            return true;
    return false;
}

从Java 8开始,有一种更简洁的使用方式 Stream.anyMatch(…)

collection.stream().anyMatch(this::checkElement);

在您的情况下,这可能看起来像这样(假设 list == entry.getValue()您的问题):

map.values().stream().anyMatch(list -> list.size() > limit);

您特定示例中的问题是对的附加调用fillUpList()。答案在很大程度上取决于此方法应该执行的操作。

旁注:就目前而言,对to的调用fillUpList()没有多大意义,因为它并不取决于您当前正在迭代的元素。我想这是剥离实际代码以适合问题格式的结果。但这恰恰导致了一个难以解释的人工例子,因此也难以推理。因此,提供Minimal,Complete可验证的示例

因此,我假设实际的代码将电流传递entry给该方法。

但是还有更多问题要问:

  • 在到达此代码之前,地图中的列表是否为空?如果是这样,为什么为什么已经有一个地图,而不仅仅是BigInteger键的列表或键集?如果它们不为空,那么为什么需要填写列表?如果列表中已经有元素,在这种情况下是更新还是其他计算?
  • 是什么导致列表变得超出限制?这是错误状态还是经常发生?是输入无效引起的吗?
  • 您是否需要计算列表,直至达到大于限制的列表?
  • 做什么 ”部分是做什么的?
  • 在这部分之后您是否重新开始填充?

当我尝试理解代码片段时,这只是我想到的一些问题。因此,在我看来,这才是真正的代码味道:您的代码并未明确传达意图。

它可能意味着这个(“全有或全无”,并且达到限制表示错误):

/**
 * Computes the list of all foo strings for each passed number.
 * 
 * @param numbers the numbers to process. Must not be {@code null}.
 * @return all foo strings for each passed number. Never {@code null}.
 * @throws InvalidArgumentException if any number produces a list that is too long.
 */
public Map<BigInteger, List<String>> computeFoos(Set<BigInteger> numbers)
        throws InvalidArgumentException
{
    if (numbers.isEmpty())
    {
        // Do you actually need to log this here?
        // The caller might know better what to do in this case...
        logger.info("Nothing to compute");
    }
    return numbers.stream().collect(Collectors.toMap(
            number -> number,
            number -> computeListForNumber(number)));
}

private List<String> computeListForNumber(BigInteger number)
        throws InvalidArgumentException
{
    // compute the list and throw an exception if the limit is exceeded.
}

或可能意味着此(“更新到第一个问题”):

/**
 * Refreshes all foo lists after they have become invalid because of bar.
 * 
 * @param map the numbers with all their current values.
 *            The values in this map will be modified.
 *            Must not be {@code null}.
 * @throws InvalidArgumentException if any new foo list would become too long.
 *             Some other lists may have already been updated.
 */
public void updateFoos(Map<BigInteger, List<String>> map)
        throws InvalidArgumentException
{
    map.replaceAll(this::computeUpdatedList);
}

private List<String> computeUpdatedList(
        BigInteger number, List<String> currentValues)
        throws InvalidArgumentException
{
    // compute the new list and throw an exception if the limit is exceeded.
}

或这样(“更新所有列表,但如果列表太大,则保留原始列表”):

/**
 * Refreshes all foo lists after they have become invalid because of bar.
 * Lists that would become too large will not be updated.
 * 
 * @param map the numbers with all their current values.
 *            The values in this map will be modified.
 *            Must not be {@code null}.
 * @return {@code true} if all updates have been successful,
 *         {@code false} if one or more elements have been skipped
 *         because the foo list size limit has been reached.
 */
public boolean updateFoos(Map<BigInteger, List<String>> map)
{
    boolean allUpdatesSuccessful = true;
    for (Entry<BigInteger, List<String>> entry : map.entrySet())
    {
        List<String> newList = computeListForNumber(entry.getKey());
        if (newList.size() > limit)
            allUpdatesSuccessful = false;
        else
            entry.setValue(newList);
    }
    return allUpdatesSuccessful;
}

private List<String> computeListForNumber(BigInteger number)
{
    // compute the new list
}

甚至以下内容(使用computeFoos(…)第一个示例,但无例外):

/**
 * Processes the passed numbers. An optimized algorithm will be used if any number
 * produces a foo list of a size that justifies the additional overhead.
 * 
 * @param numbers the numbers to process. Must not be {@code null}.
 */
public void process(Collection<BigInteger> numbers)
{
    Map<BigInteger, List<String>> map = computeFoos(numbers);
    if (isLimitReached(map))
        processLarge(map);
    else
        processSmall(map);
}

private boolean isLimitReached(Map<BigInteger, List<String>> map)
{
    return map.values().stream().anyMatch(list -> list.size() > limit);
}

否则可能意味着完全不同的…;-)

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.