web-dev-qa-db-ja.com

ネストされたIFステートメントをどのようにリファクタリングしますか?

GOTOに関するこの投稿に遭遇したとき、私はプログラミングブロゴスフィアを巡っていました。

http://giuliozambon.blogspot.com/2010/12/programmers-tabu.html

ここで作者は、「GOTOがコードをより読みやすく、より保守しやすくするという状況があるという結論に達しなければならない」と述べ、次に、次のような例を示します。

if (Check#1)
{
    CodeBlock#1
    if (Check#2)
    {
        CodeBlock#2
        if (Check#3)
        {
            CodeBlock#3
            if (Check#4)
            {
                CodeBlock#4
                if (Check#5)
                {
                    CodeBlock#5
                    if (Check#6)
                    {
                        CodeBlock#6
                        if (Check#7)
                        {
                            CodeBlock#7
                        }
                        else
                        {
                            rest - of - the - program
                        }
                    }
                }
            }
        }
    }
}

次に、ライターは、GOTOを使用すると、このコードが非常に読みやすく、保守しやすくなると提案しています。

個人的には、フローを壊すGOTOに頼らずに、コードを平坦化してこのコードを読みやすくするために、少なくとも3つの異なる方法を考えることができます。これが私の2つのお気に入りです。

1-ネストされた小さな関数。各ifとそのコードブロックを取得して、関数に変換します。ブール値チェックが失敗した場合は、戻ります。成功した場合は、チェーン内の次の関数を呼び出します。 (男の子、それは再帰のように聞こえますが、関数ポインターを使用して単一のループでそれを行うことができますか?)

2-センチネル変数。私にはこれが一番簡単です。 blnContinueProcessing変数を使用して、ifチェックでまだtrueかどうかを確認してください。次に、チェックが失敗した場合は、変数をfalseに設定します。

ネストを減らして保守性を向上させるために、このタイプのコーディング問題をリファクタリングできる方法はいくつありますか?

34
saunderl

さまざまなチェックがどのように相互作用するかを知らずに判断するのは本当に難しいです。厳密なリファクタリングが必要かもしれません。タイプに応じて正しいブロックを実行するオブジェクトのトポロジーを作成すると、戦略パターンまたは状態パターンもうまくいく場合があります。

最善の方法がわからない場合は、メソッドをさらに抽出することでさらにリファクタリングできる2つの単純なリファクタリングを検討します。

メソッドのlitleの出口点として常に好きなので、私は本当に嫌いな最初の1つ(できれば1つ)

if (!Check#1)
{ 
    return;
}
CodeBlock#1

if (!Check#2)
{
    return;
}
CodeBlock#2
...

2つ目は、複数のリターンを削除しますが、ノイズも多く追加します。 (基本的にはネストを削除するだけです)

bool stillValid = Check#1
if (stillValid)
{
  CodeBlock#1
}

stillValid = stillValid && Check#2
if (stillValid)
{
  CodeBlock#2
}

stillValid = stillValid && Check#3
if (stillValid)
{
  CodeBlock#3
}
...

この最後のものは、関数にうまくリファクタリングでき、適切な名前を付けると、結果は妥当なものになる可能性があります。

bool stillValid = DoCheck1AndCodeBlock1()
stillValid = stillValid && DoCheck2AndCodeBlock2()
stillValid = stillValid && DoCheck3AndCodeBlock3()

public bool DoCheck1AndCodeBlock1()
{
   bool returnValid = Check#1
   if (returnValid)
   {
      CodeBlock#1
   }
   return returnValid
}

全体として、おそらくより良い方法があります

33
KeesDijk

これは、適切なインデントのあるコードの形状のため、「矢印コード」と呼ばれます。

Jeff Atwoodは、矢印を平らにする方法についてCoding Horrorに関する良いブログ投稿をしました:

フラット化矢印コード

完全な扱いについては記事を読んでください、しかしここに主要なポイントがあります。

  • 条件をガード句に置き換える
  • 条件付きブロックを個別の関数に分解する
  • 否定的なチェックを肯定的なチェックに変換する
  • 常に便宜的に関数からできるだけ早く戻る
40
JohnFx

後藤だと主張する人もいますが、return;は明らかなリファクタリングです。

if (!Check#1)
{ 
        return;
}
CodeBlock#1
if (!Check#2)
{
    return;
}
CodeBlock#2
.
.
.
if (Check#7)
{
    CodeBlock#7
}
else
{
    rest - of - the - program
}

それがコードを実行する前の一連のガードチェックである場合、これは正常に機能します。それよりも複雑な場合は、少し単純になるだけで、他のソリューションの1つが必要になります。

26
Richard Gadsden

このスパゲッティコードは、ステートマシンにリファクタリングするのに最適な候補のようです。

10
Pemdas

これはすでに言及されているかもしれませんが、ここに示されている「矢じりアンチパターン」に対する私の「頼りになる」(しゃれた意図の)答えは、ifsを逆にすることです。現在の条件の反対をテストし(not演算子で十分簡単)、それがtrueの場合はメソッドから戻ります。ゴトスはありません(はっきりと言えば、voidリターンは、呼び出しコードの行へのジャンプに過ぎず、スタックからフレームをポップするという簡単な追加のステップがあります)。

適例、ここにオリジナルがあります:

if (Check#1)
{
    CodeBlock#1
    if (Check#2)
    {
        CodeBlock#2
        if (Check#3)
        {
            CodeBlock#3
            if (Check#4)
            {
                CodeBlock#4
                if (Check#5)
                {
                    CodeBlock#5
                    if (Check#6)
                    {
                        CodeBlock#6
                        if (Check#7)
                        {
                            CodeBlock#7
                        }
                        else
                        {
                            rest - of - the - program
                        }
                    }
                }
            }
        }
    }
}

リファクタリング:

if (!Check#1) return;

CodeBlock#1

if (!Check#2) return;

CodeBlock#2

if (!Check#3) return;

CodeBlock#3

if (!Check#4) return;

CodeBlock#4

if (!Check#5) return;

CodeBlock#5

if (!Check#6) return;

CodeBlock#6

if (Check#7)
    CodeBlock#7
else
{
    //rest of the program
}

それぞれの場合、基本的に、続行する必要があるかどうかを確認します。ネストのレベルが1つだけで、オリジナルとまったく同じように機能します。

このスニペットの末尾を超えて実行する必要があるものがある場合は、このコードを独自のメソッドに抽出し、このコードが現在存在する場所から呼び出してから、このスニペットの後に続くコードに進んでください。スニペット自体は十分に長く、各コードブロックに実際のLOCが十分に与えられているため、さらにいくつかのメソッドを分割することを正当化することができますが、余談です。

6
KeithS

ifチェックのこのピラミッドを実際に必要とするロジックを日常的に使用している場合、おそらく(比喩的に)レンチを使用して釘を打ち込んでいます。そのようなねじれた複雑なロジックを線形よりも優れた構造でサポートする言語で、このようなねじれた複雑なロジックを実行するほうがよいでしょうif/else if/elseスタイルの構成。

この種の構造により適した言語には、SNOBOL4(奇妙なデュアルGOTOスタイルの分岐を伴う)またはPrologMercury(それらの統一を伴う)などの論理言語が含まれます。そして、複雑な決定をかなり簡潔に表現するためのDCGは言うまでもなく、バックトラック機能。

もちろん、これがオプションではない場合(ほとんどのプログラマーは悲劇的なことに、多声ではないため)、他の人が思いついたアイデアは、さまざまな [〜#〜] oop [〜#〜] - structures または 句を関数に分割 または、もしあなたが絶望的で、ほとんどの人が 状態マシン

しかし、私の解決策は、表現しようとしているロジックを(これがコード内で一般的であると仮定して)より簡単で読みやすい方法で表現できる言語に到達し続けています。

ここ から:

パックマンのifのセットを見ていると、よくあることですが、関係するすべての条件の真理値表のようなものを引き出せば、問題を解決するためのはるかに優れたルートを見つけることができます。

そうすれば、より良い方法があるかどうか、どのようにそれをさらに分解できるか(そしてこれはこの種のコードでは大きな問題です)、ロジックに穴があるかどうかを評価することもできます。

それができたら、おそらくそれをいくつかのswitchステートメントといくつかのメソッドに分解し、コード全体を調べなければならない次の貧しいムックを救うことができます。

1
Jim G.

個人的には、これらのifステートメントを個別の関数にラップして、関数が成功した場合にブール値を返すのが好きです。

構造は次のようになります。


if (DoCheck1AndCodeBlock1() && 
    DoCheck2AndCodeBlock2() && 
    DoCheck3AndCodeBlock3()) 
{
   // ... you may perform the final operation here ....
}

唯一の欠点は、これらの関数が参照またはメンバー変数によって渡される属性を使用してデータを出力する必要があることです。

1
gogisan

OOを使用すると、リーフが単純な条件であり、コンポーネントが単純な要素の結合である複合パターンにアプローチし、この種のコードを拡張可能かつ適応可能にします

1
guiman

可能なオブジェクト指向のソリューションが必要な場合、コードは 責任の連鎖設計パターン を使用したリファクタリングの標準的な例のようになります。

0
FinnNk

それらを関数に分割すると役立つでしょうか?

最初の関数を呼び出し、それが完了すると次を呼び出します。関数が最初に行うことは、その関数のチェックをテストすることです。

0
Toby

それは各コードブロックのサイズと合計サイズに大きく依存しますが、私はブロックのストレート「抽出メソッド」によって、または無条件の部分を個別のメソッドに移動することによって分割する傾向があります。ただし、前述の@KeesDijkの警告が適用されます。前者のアプローチでは、次のような一連の機能が提供されます

if (Check#1)
{
    CodeBlock#1
    NextFunction
}

これはうまく機能しますが、コードの膨張と「1度しか使用されないメソッド」アンチパターンにつながります。だから私は一般的にこのアプローチを好みます:

if (CheckFunctionOne)
{
    MethodOneWithDescriptiveName
    if (CheckFunctionTwo)
    {
        MethodTwoWithDescriptiveName
        ....

プライベート変数とパラメータの適切な使用により、それを渡すことができます。文芸的なプログラミングをちらっと見たことが、ここで役立ちます。

0
Мסž