1つの画面サイズよりも大きい関数を記述してはならない、頻繁に呼び出す場合は関数に抽出する必要がある、およびこれらのコーディングガイドラインすべてをよく耳にします。それらの1つは、インデントレベルを低く保つことです。一部の人々は私が最大インデントを4回にするべきであるとさえ私に言った。
しかし、毎日のプログラミングでは、なんらかのフロー制御でO(n ^ 2)またはO(n ^ 3)ループが必要になるということが起こり続けます。
それを避ける良い方法はありますか? 1つの問題は、複雑なif/elseステートメントのネストを回避しようとすると、1つの行でこれを行う傾向があり、非常にひどく、読みにくく、保守が非常に困難になる可能性があります。
すでにお気づきのように、インデントを減らすために制御ロジックをまとめることは完全に逆効果です。問題は実際にはインデントのレベルや行数ではありません。これらのことは、実際の問題の指標にすぎません。1つのコードでやりすぎていることです。
解決策は、ネストされた複雑なループを別々のメソッドにリファクタリングして、呼び出し側と呼び出されたメソッドの両方を1つの賢明なこととして理解できるようにすることです。ループで必要なローカル変数が多すぎて簡単に分離できない場合は、ローカルオブジェクトのスコープが大きい変数を新しい特殊用途のミニクラスのクラススコープにカプセル化するメソッドオブジェクトが役立ちます。
同じことが複雑なコントローラーロジックにも当てはまります。ループの継続または停止の条件に多数の複雑な式が含まれている場合は、それを適切な名前のブールメソッドにリファクタリングします。その目的は、補助関数が正しく、ヘルパー関数が非常に小さいために自分で簡単に検証できる場合、トップレベルのロジックを明らかににすることです。
反転した場合
ほとんどの開発者はネストされたロジックの観点から考えています。ネストを減らすためにロジックを逆にすることは困難です。深くネストされたコードは読みにくく、ネストを減らすためにロジックを反転する必要があることがよくあります。
プログラマーがコードを書くとき、彼らはこのような論理ツリー構造で書きます。
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
によって処理されます。
結論
単一の目的と逆論理がインデントを減らすための鍵です。
これらのガイドラインの背後にある考え方は、あなたがあなたのような状況にいるときに、アルゴリズムに問題がある可能性が最も高い(常にではないが、考えているよりも多い)ので、前に戻る必要があるということです。考え直してください。
実際にO(n²)またはO(n³)アルゴリズムである場合、リファクタリングがそれほど役立つことはないと私は実際には考えていません。(十分に広い端末を持つ人々にとって)読みやすさがさらに悪化し、速度が低下する可能性があります。 (もちろんifリファクタリングすることで状況を改善できますwithout何も失うことはありません、ぜひそうしてください!)本当に、一歩下がって、同僚や他の誰かに見てもらいましょうあなたのコードとアルゴリズムでアドバイスを与えてください…そして何も助けにならないなら、もちろんその上に詳細なコメントを付けて、その1つの関数のガイドラインを無視してください。
if (condition) {big long nested code}
の代わりにif (!condition) return;
を使用しますそれを避ける良い方法はありますか? 1つの問題は、複雑なif/elseステートメントのネストを回避しようとすると、1つの行でこれを行う傾向があり、非常にひどく、読みにくく、保守が非常に困難になる可能性があります。
ローカル関数を使用して、これらの条件文の本体を具体化します。これを行うことで、ワークフロー(スーパーバイザー)と変換(実行者)の間の線が途切れ、明瞭さが向上します。
一見すると、ローカル関数を追加すると、プログラムの読み取りフローが中断されるため読み取りが難しくなりますが、実際には、ワークフローと変換が分離されているため、わかりやすさが向上します。