後で使用するためにループにフラグを設定するのはコードの匂いですか?


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
なぜコードの匂いだと感じますか?これを行うときに、異なる構造では発生しない、どのような特定の問題を予見できますか?
ベンコットレル

13
@ gnasher729好奇心から、どの用語を代わりに使用しますか?
ベンコットレル

11
-1、あなたの例は意味がありません。entry関数ループ内ではどこにも使用されておらず、何であるかしか推測できませんlist。されてfillUpList埋めることになってlist?なぜパラメーターとして取得しないのですか?
Doc Brown

13
空白と空行の使用を再検討します。
ダニエルジュール

11
コードの匂いのようなものはありません。「コードの匂い」は、エリート主義の基準を満たさないコードを見たときに鼻をかみたいソフトウェア開発者によって発明された用語です。
ロバートハーヴェイ

回答:


70

バイナリーの区別を記録するという目的でブール値を使用することには何の問題もありません。

このコードをリファクタリングするように言われたら、おそらくループを独自のメソッドに入れて、代入+ breakreturn;になるようにします。変数さえ必要ありません、あなたは単に言うことができます

if(fill_list_from_map()) {
  ...

6
実際、彼のコードの匂いは、小さな関数に分割する必要がある長い関数です。あなたの提案が進むべき道です。
ベルンハルトヒラー

2
そのコードの最初の部分の有用な機能を説明するより良いフレーズは、それらのマップされたアイテムから何かを蓄積した後に制限を超えるかどうかを見つけることです。またfillUpList()、実際にはentry反復からの値を使用するコード(OPは共有しないことを決定する)であると安全に想定できます。この仮定がなければ、ループ本体はループの反復から何も使用しなかったように見えます。
rwong

4
@キリアン:私はただ一つ懸念があります。このメソッドはリストを埋め、リストのサイズが制限を超えているかどうかを表すブール値を返すため、「fill_list_from_map」という名前は、返されるブール値が何を表すかを明確にしない制限を超えるなど)。返されるブール値は、関数名からはわからない特別な場合のためのものです。コメントはありますか?PS:コマンドクエリの分離も考慮することができます。
シッダールストリカ

2
@SiddharthTrikhaあなたは正しいです、そしてそのラインを提案したとき、私はまったく同じ懸念を持っていました。しかし、コードがどのリストを埋めるべきかは不明です。常に同じリストである場合は、フラグは必要ありません。後でその長さを確認できます。個々の満杯が制限を超えたかどうかを知る必要がある場合は、何らかの方法でその情報を外部に転送する必要があり、コマンド/クエリ分離の原則は明白な方法を拒否する十分な理由ではありません:値。
キリアンフォス

6
ボブおじさんは、クリーンコードの 45ページで次のように述べています。混乱。"
CJデニス

25

それは必ずしも悪いわけではなく、時にはそれが最良の解決策である場合もあります。ただし、ネストされたブロックでこのようなフラグを設定すると、コードを追跡しにくくなる可能性があります。

問題は、スコープを区切るブロックがありますが、その後、スコープ間で通信するフラグがあり、ブロックの論理的な分離を壊すことです。例えば、limitFlag場合はfalseになりますmapnullあれば、「何かをする」-codeが実行されるように、mapですnull。これは意図したものかもしれませんが、このフラグの条件はネストされたスコープ内の別の場所で定義されているため、見落としやすいバグになる可能性があります。情報とロジックを可能な限り狭い範囲に収めることができる場合は、そうするようにしてください。


2
これは、ブロックが完全に分離されておらず、後で追跡するのが難しい可能性があるため、コードの臭いだと感じた理由です。だから私は@Kilianの答えのコードが私たちが得ることができる最も近いと思いますか?
シッダールストリカ

1
@SiddharthTrikha:コードが実際に何をするのかわからないので、言うのは難しいです。マップに制限よりも大きいリストのアイテムが少なくとも1つ含まれているかどうかだけを確認したい場合は、1つのanyMatch式でそれを行うことができると思います。
ジャックB

2
@SiddharthTrikha:スコープの問題は、初期テストを次のようなガード句に変更することで簡単に解決できますが、表示されるif(map==null || map.isEmpty()) { logger.info(); return;}コードが関数の完全なボディであり// Do something、マップの場合はその部分が不要な場合にのみ機能しますnullまたは空です。
ドックブラウン

14

「コードのにおい」について推論することはお勧めしません。それは、あなた自身の偏見を合理化するための可能な限り最も怠wayな方法です。時間が経つにつれて、あなたは多くのバイアスを開発し、それらの多くは合理的になりますが、それらの多くは愚かです。

代わりに、ある事柄を別の事柄よりも優先するという実用的な(つまり独断的な)理由を持たせ、同様の質問すべてに対して同じ答えをすべきだと考えることを避けてください。

「コードの匂い」は、あなた考えていないときのためのものです。あなたが本当にコードについて考えるつもりなら、それを正しくしてください!

この場合、周囲のコードに応じて、決定は実際にどちらの方向にも進む可能性があります。それは、コードが何をしているのかを考える最も明確な方法であると思うものに本当に依存します。(「クリーンな」コードとは、他の開発者に自分のやっていることを明確に伝え、正しいことを簡単に確認できるコードです)

多くの場合、人々はフェーズごとに構造化されたメソッドを記述します。コードは、最初にデータについて知っておくべきことを判断し、次にそれを実行します。「決定する」部分と「それに対応する」部分の両方が少し複雑な場合、これを行うのは理にかなっており、多くの場合、ブールフラグのフェーズ間で「知っておくべきこと」を伝えることができます。ただし、旗にもっと良い名前を付けた方がいいと思います。「largeEntryExists」のようなものは、コードをよりきれいにします。

一方、「// Do Something」コードが非常に単純な場合if、フラグを設定する代わりにブロック内に配置する方が理にかなっています。これにより、効果が原因により近くなり、読者はフラグが設定した値を保持していることを確認するために残りのコードをスキャンする必要がなくなります。


5

はい、それはコードのにおいです(それを行うすべての人からの手がかりです)。

私にとって重要なことは、breakステートメントの使用です。それを使用しなかった場合、必要以上の項目を繰り返し処理することになりますが、これを使用すると、ループから2つの可能な出口点が得られます。

あなたの例では大きな問題ではありませんが、ループ内の条件がより複雑になったり、初期リストの順序が重要になったりすると、バグがコードに忍び込みやすくなります。

コードが例のように単純な場合、whileループまたは同等のマップ、フィルター構成に縮小できます。

コードがフラグとブレークを必要とするほど複雑な場合、バグが発生しやすくなります。

すべてのコードの匂いと同様に:フラグが表示された場合は、に置き換えてみてくださいwhile。できない場合は、ユニットテストを追加します。


私から+1。それは確かにコードの匂いであり、あなたはそれをなぜ、どのように処理するかを明確に述べています。
デビッドアルノ

@ユアン:SO as with all code smells: If you see a flag, try to replace it with a while例を挙げてこれについて詳しく説明していただけますか?
シッダールストリカ

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は少し錆びていますが、マップを使用している場合は、コレクタを使用してアイテムの数を合計し、制限を超えたものを除外します。しかし、例が「それほど悪くない」と言っているように、コードの匂いは潜在的な問題を警告する一般的なルールです。常に従わなければならない神聖な法律ではない
ユアン

1
「キュー」ではなく「キュー」を意味しませんか?
psmears

0

実際にチェックしていることを示すlimitFlag以外の名前を使用してください。また、マップが存在しない場合や空の場合、何も記録しないのはなぜですか?limtFlagはfalseになります、あなたが気にするすべて。マップが空の場合、ループは正常であるため、チェックする必要はありません。


0

既に持っていた情報を伝えるためにブール値を設定することは、私の意見では悪い習慣です。簡単な代替手段がない場合は、おそらくカプセル化の不足などの大きな問題を示しています。

forループロジックをfillUpListメソッドに移動して、制限に達した場合にブレークするようにする必要があります。その後、リストのサイズをすぐに確認します。

それがあなたのコードを壊すなら、なぜですか?


0

まず一般的なケース:フラグを使用して、コレクションの特定の要素が特定の条件を満たすかどうかを確認することは珍しくありません。しかし、私がこれを解決するために最も頻繁に見たパターンは、追加のメソッドでチェックを移動し、それから直接戻ることです(彼の答えで説明されいるキリアン・フォスのように):

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()です。答えは、このメソッドの実行内容に大きく依存します。

サイドノート:現状では、への呼び出しfillUpList()はあまり意味がありません。なぜなら、それは現在反復している要素に依存しないからです。これは、質問の形式に合わせて実際のコードを削除した結果だと思います。しかし、まさにそれは、解釈が難しく、したがって推論するのが難しい人為的な例につながります。したがって、最小限、完全検証可能なを提供することが非常に重要です。

したがって、実際のコード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);
}

または、まったく異なるものを意味する可能性があります... ;-)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.