web-dev-qa-db-ja.com

インデントレベルを低く保つ

1つの画面サイズよりも大きい関数を記述してはならない、頻繁に呼び出す場合は関数に抽出する必要がある、およびこれらのコーディングガイドラインすべてをよく耳にします。それらの1つは、インデントレベルを低く保つことです。一部の人々は私が最大インデントを4回にするべきであるとさえ私に言った。

しかし、毎日のプログラミングでは、なんらかのフロー制御でO(n ^ 2)またはO(n ^ 3)ループが必要になるということが起こり続けます。

それを避ける良い方法はありますか? 1つの問題は、複雑なif/elseステートメントのネストを回避しようとすると、1つの行でこれを行う傾向があり、非常にひどく、読みにくく、保守が非常に困難になる可能性があります。

10
reox

すでにお気づきのように、インデントを減らすために制御ロジックをまとめることは完全に逆効果です。問題は実際にはインデントのレベルや行数ではありません。これらのことは、実際の問題の指標にすぎません。1つのコードでやりすぎていることです。

解決策は、ネストされた複雑なループを別々のメソッドにリファクタリングして、呼び出し側と呼び出されたメソッドの両方を1つの賢明なこととして理解できるようにすることです。ループで必要なローカル変数が多すぎて簡単に分離できない場合は、ローカルオブジェクトのスコープが大きい変数を新しい特殊用途のミニクラスのクラススコープにカプセル化するメソッドオブジェクトが役立ちます。

同じことが複雑なコントローラーロジックにも当てはまります。ループの継続または停止の条件に多数の複雑な式が含まれている場合は、それを適切な名前のブールメソッドにリファクタリングします。その目的は、補助関数が正しく、ヘルパー関数が非常に小さいために自分で簡単に検証できる場合、トップレベルのロジックを明らかににすることです。

15
Kilian Foth

反転した場合

ほとんどの開発者はネストされたロジックの観点から考えています。ネストを減らすためにロジックを逆にすることは困難です。深くネストされたコードは読みにくく、ネストを減らすためにロジックを反転する必要があることがよくあります。

プログラマーがコードを書くとき、彼らはこのような論理ツリー構造で書きます。

 bool function publish(Document doc)
 {
    if(doc != null)
    {
        if(doc.status == "finished")
        {
            doc.print();
            if(doc.locked)
            {
                doc.unlock();
            }

            return true;
        }

        return false;
    }

    return false;
 }

上記は論理的に正しいですが、インデントが導入されており、将来の維持が困難になります。追加のロジックにはより深いブロックが必要になるため、これは困難です。深さが深いほど、バグが発生する可能性が高くなります。

Ifステートメントを反転して組み合わせることにより、ネストを減らすことができます。

 bool function publish(Document doc)
 {
    if(doc == null || doc.status != "finished")
    {
        return false
    }

    doc.print();

    if(!doc.locked)
    {
        return true;
    }

    doc.unlock();

    return true;
 }

この逆の種類のロジックは、すべての関数操作を最初のインデントされたブロック内に保持し、ifステートメントは関数の終了にのみ使用されます。

スプリットループと作業

ループブロック内で作業を実行しないでください。タスク自体からイテレーションを分離します。これにより、単一の目的の機能が作成されます。

この例を取る

function void PrintDocuments(List<Document> pDocuments)
{
    for(int doc=0; doc < pDocuments.Count; doc++)
    {
        for(int page=0; page < pDocuments[doc].Pages.Count; page++)
        {
            pDocuments[doc].Pages[page].Print();
        }
    }
}

これはそれほど複雑ではありませんが、3レベルのネストが作成され、関数は3つの異なるタスクを実行します。

作業とループを分割すると、保守が容易になります。

function void PrintDocuments(List<Document> pDocuments)
{
    for(int doc=0; doc < pDocuments.Count; doc++)
    {
        PrintPages(pDocuments[doc].Pages);
    }
}

private function void PrintPages(Document pDocument)
{
    for(int page=0; page < pDocument.Pages.Count; page++)
    {
        PrintPage(Pages[page]);
    }
}

private function void PrintPage(Page pPage)
{
    pPage.Print();
}

上記は余分な作業であると主張する人もいますが、タスクを分割して単一目的の関数を作成するために費やされる時間は、後でコードを保守するときの労力を削減します。インデントを減らすと、読みやすくなります。ループによって完了する作業は、ループ内では実行されませんが、その作業専用の関数PrintPageによって処理されます。

結論

単一の目的逆論理がインデントを減らすための鍵です。

13
Reactgular

これらのガイドラインの背後にある考え方は、あなたがあなたのような状況にいるときに、アルゴリズムに問題がある可能性が最も高い(常にではないが、考えているよりも多い)ので、前に戻る必要があるということです。考え直してください。

実際にO(n²)またはO(n³)アルゴリズムである場合、リファクタリングがそれほど役立つことはないと私は実際には考えていません。(十分に広い端末を持つ人々にとって)読みやすさがさらに悪化し、速度が低下する可能性があります。 (もちろんifリファクタリングすることで状況を改善できますwithout何も失うことはありません、ぜひそうしてください!)本当に、一歩下がって、同僚や他の誰かに見てもらいましょうあなたのコードとアルゴリズムでアドバイスを与えてください…そして何も助けにならないなら、もちろんその上に詳細なコメントを付けて、その1つの関数のガイドラインを無視してください。

6
mirabilos
  • 最も内側のループを関数に入れます。これは読みやすさにも役立ちます。
  • コードを入れ子ではなく短絡に再配置します。たとえば、if (condition) {big long nested code}の代わりにif (!condition) return;を使用します
  • 入れ子をあまり必要としない中間形式にデータを前処理します。たとえば、リンクリストを最初に配列にコピーすると、リンクリストを横断するネストされたループを後で回避できる場合があります。
  • ネストされたループの代わりに再帰を使用します。再帰は、深くネストされたコードを大幅に簡略化することがよくあります。
  • すべてのレベルでチェックするのではなく、例外を使用して、エラー条件の間にネストを終了します。
  • O(nよりも効率的なアルゴリズムを見つけるようにしてください)。
2
Karl Bielefeldt

それを避ける良い方法はありますか? 1つの問題は、複雑なif/elseステートメントのネストを回避しようとすると、1つの行でこれを行う傾向があり、非常にひどく、読みにくく、保守が非常に困難になる可能性があります。

ローカル関数を使用して、これらの条件文の本体を具体化します。これを行うことで、ワークフロー(スーパーバイザー)と変換(実行者)の間の線が途切れ、明瞭さが向上します。

一見すると、ローカル関数を追加すると、プログラムの読み取りフローが中断されるため読み取りが難しくなりますが、実際には、ワークフローと変換が分離されているため、わかりやすさが向上します。

1
user40989