私は休日の週末に少しCを学び、Cで書かれた他のプログラムを見始めました。それが良い例になると思い、GNUNetcatを見ることになりました。
600行のmain()
機能を見て少しショックを受けました。これは正常ですか?それが正常である場合、これは良いCコーディング慣行と見なされますか?
私は休日の週末に少しCを学び、Cで書かれた他のプログラムを見始めました。それが良い例になると思い、GNUNetcatを見ることになりました。
600行のmain()
機能を見て少しショックを受けました。これは正常ですか?それが正常である場合、これは良いCコーディング慣行と見なされますか?
男性の足の長さを尋ねられたアメリカの大統領(リンカーン?)の引用があります。「彼の体から地面に達するのに十分な長さ」と彼は言った。
トピックに戻る:
「CleanCode」のような本の著者は、すべての関数が1つのことだけを実行することを宣伝しています(ここでは私が大幅に簡略化しています)。したがって、理論的にmain()
は、初期化関数を呼び出してから、アプリケーションの作業を調整する別の関数を呼び出す必要があります。
実際には、多くのプログラマーは多くの小さな機能を苛立たせていると感じています。おそらくより有用なメトリックは、関数が見やすく、考えやすくするためだけに、通常は1つの画面に収まる必要があるということです。
プログラムが複雑で、その機能のほとんどがmain()
組み込まれている場合、誰かが問題を解決するための適切な仕事をしていません。基本的に、管理性、理解性、読みやすさを追求する必要があります。通常、main()が巨大になる理由はありません。
特定の種類のアプリケーションでは、main()に数百行の初期化があり、その後に約20行のトップレベルループが続くことがよくあります。
関数を2回呼び出す必要があるまで、関数を分割しないのが私の習慣です。これにより、300行の関数を作成する場合がありますが、同じブロックが2回発生するのを確認するとすぐに、そのブロックを分割します。
mainに関しては、初期化ルーチンは1回であることが多いため、600行が不合理に聞こえることはありません。
言語に関係なく、サブルーチンメソッドをコードの1ページに表示されるものに大まかに制限し、可能な限りサブルーチンに機能を抽出しようとします。
600行は、どの実装でもかなり長く聞こえます。おそらく、いくつかの最も重要な理由があります。議論と明快さを渡す(私はあなたが投稿した例を見ていません)が、それは通常行われていることの遠端にあるように聞こえます、そしてこのタスクを細分化することは可能であるはずです。
私はそれが何年にもわたって機能の継続的な漸進的な追加によって開発されたと思います、そして誰もこれを止めて、より読みやすく/維持しやすいようにリファクタリングしました。このための単体テストがない場合(そして私の経験では、main()
何らかの理由でメソッドが記述されたテストを取得することはあまりありません)、それをリファクタリングすることには理解できる抵抗があります。
ええ、それはひどいですが、私はもっと悪く見えました。サブルーチンがまったくない、大規模な数千行のFortranプログラムを見てきました。
答えは次のとおりです。エディターウィンドウに収まり、循環的複雑度が低くなければなりません。
メインプログラムが単なる一連の関数呼び出しまたは計算である場合、それは必要なだけ長くなる可能性があり、エディターウィンドウの制約から免除される可能性があると思います。それでも、意味のある個別のメソッドを抽出する自然な方法がなかったことに少し驚かされます。
ただし、テストと分岐、およびreturn
ingとbreak
ingとcontinue
-ingの場合は、個別にテストされた機能コンポーネントに分割する必要があります。
うまくいけば、彼らはリファクタリングを計画しています。これは非常にラフに見えます。
443 while (optind < argc) {
444 const char *get_argv = argv[optind++];
445 char *q, *parse = strdup(get_argv);
446 int port_lo = 0, port_hi = 65535;
447 nc_port_t port_tmp;
448
449 if (!(q = strchr(parse, '-'))) /* simple number? */
450 q = strchr(parse, ':'); /* try with the other separator */
451
452 if (!q) {
453 if (netcat_getport(&port_tmp, parse, 0))
454 netcat_ports_insert(old_flag, port_tmp.num, port_tmp.num);
455 else
456 goto got_err;
457 }
458 else { /* could be in the forms: N1-N2, -N2, N1- */
459 *q++ = 0;
460 if (*parse) {
461 if (netcat_getport(&port_tmp, parse, 0))
462 port_lo = port_tmp.num;
463 else
464 goto got_err;
465 }
466 if (*q) {
467 if (netcat_getport(&port_tmp, q, 0))
468 port_hi = port_tmp.num;
469 else
470 goto got_err;
471 }
472 if (!*parse && !*q) /* don't accept the form '-' */
473 goto got_err;
474
475 netcat_ports_insert(old_flag, port_lo, port_hi);
476 }
477
478 free(parse);
479 continue;
480
481 got_err:
482 free(parse);
483 ncprint(NCPRINT_ERROR, _("Invalid port specification: %s"), get_argv);
484 exit(EXIT_FAILURE);
485 }
600ラインのメインはちょっとした警告サインです。しかし、あなたがそれを見て、これを行う以外にそれをより小さな断片に分割する方法を見ることができない場合。
void the_first_part_of_main(args...);
void the_second_part_of_main(args...);
...
main()
{
the_first_part_of_main();
the_second_part_of_main();
...
}
次に、それを放っておく必要があります。
一部の標準では、600行の関数を使用することは悪い考えですが、mainを他の関数とは異なる方法で処理する必要がある理由はありません。
このような状況が発生していると私が考えることができる唯一の理由は、プログラムが急速に開発され、それが成長するにつれて、誰もそれをより論理的な単位に分割することを気にしないということです。
できるだけ短くします。通常、名前を割り当てることができる操作があるときはいつでも、そのための新しいメソッドを作成します。
私はあなたのルーチンが効果的で確実にそして自動的にテストされるために必要なだけ長く/短くあるべきだと思います。600ステートメントのルーチンには複数のパスが含まれている可能性があり、ルーチンの組み合わせは非常に急速に大きくなる可能性があります。関数を読みやすいものに分解しようとしています。機能は「機能的」または「物語的」のいずれかです。ユニットテストを含めてずっと。
私の個人的なコーディングスタイルは、コマンドライン引数の解析と、プログラムに必要な高額な初期化のためにmain関数のみを使用することです。
私が見た600行の関数のほぼすべてもばかげて書かれていました。これはそうである必要はありません。
ただし、これらの場合、プログラマーがズームアウトしたビューを表示し、セクションに意味のある名前を付けるのは失敗でした-高レベル(Initialize()など)と低レベル(一般的な3をとる何か)の両方-線のパターンと、パラメータを使用して1つの名前で非表示にします)。
極端な愚かさの場合、彼らはそれが必要とされていないときに関数呼び出しのパフォーマンスを最適化していました。
main()
、他の関数と同様に、必要なだけ大きくする必要があります。「必要に応じて」は、何をする必要があるかによって大きく異なります。そうは言っても、ほとんどの場合、数百行を超える必要はありません。600行は少し多めですが、その一部は別の関数にリファクタリングできる/おそらくリファクタリングする必要があります。
極端な例として、私が所属していた1つのチームは、3Dディスプレイを駆動するためのコードを高速化するという任務を負っていました。このコードは元々、昔ながらのFORTRANを使用してプログラミングを学んだワイヤーヘッドによって書かれました。5,000main()
行を超えるコードがあり、ランダムなビットがあちこちで編集されていました。コードを関数に分割する代わりに、彼は単にvia内のサブルーチンに分岐します(13から15のgotoの間のどこかで、一見ランダムに両方向に分岐します)。最初のステップとして、レベル1の最適化をオンにしました。コンパイラはすぐに利用可能なすべてのメモリとスワップスペースを飲み込み、カーネルをパニックにしました。コードが非常に壊れやすいため、作成できませんでした#include
main()
goto
何かを壊すことなく変化します。最後に、2つの選択肢があることをお客様に伝えました。システム全体を最初から書き直すか、より高速なハードウェアを購入するかです。
彼らはより速いハードウェアを購入しました。