web-dev-qa-db-ja.com

なぜ短い関数を使用してコードを区分化するのですか?

プログラミングの世界では、コードブロックを独自の関数に分離することをお勧めします。明らかに、そのコードブロックが再利用可能であれば、それを行う必要があります。私が理解していないのは、関数呼び出しを基本的にコメントとして使用するこの傾向であり、コードが再利用可能でない場合はコードを隠します。それがコード折りたたみの目的です。

個人的には、GOTOステートメントと同じ問題があるように感じるので、このようなコードの読み取りも嫌いです。プログラムのフローを追跡しようとすると、常にジャンプして論理的にたどることができないスパゲッティコードになります。コード。線形であるが、それが何をするかを示すコードのセクションに単一のコメントがあるコードをフォローする方がはるかに簡単です。コードの折りたたみでは、これは基本的にまったく同じですが、コードが適切な線形の方法でとどまります。これを同僚に説明しようとすると、コメントは邪悪で乱雑であると彼らは言っています-折りたたまれたコードのブロックの上にあるコメントは、一度しか呼び出されない関数呼び出しとどう違うのですか?関数の使いすぎとコメントの使いすぎはどう違うのですか?関数の頻繁な使用は、GOTOステートメントの問題とどのように異なりますか?誰かがプログラミングパラダイムの価値を私に説明してくれませんか?

20
dallin

コードの編成とは、単一のアイデアを伝えるのに十分な情報を表示することです。スイートスポットは、1つのアイデアが1つのコードユニットに収まるようにコードを十分に整理することです。コードの単位は、関数、クラスなどにすることができます。これらは、単に整理のツールです。他のツールと同様に、使いすぎたり、誤って使用されたりする可能性があります。

関数が意味のあるアイデアを伝えない限り、1行の関数を持つことは意味がありません。多くのアイデアを伝える大きな必須の機能を持つことは、消化して再利用するのが困難です。それはすべて適切なバランスをとることに関するものであり、それも主観的なものです。

30
mortalapeman

プログラミングの世界では、コードブロックを独自の関数に分離することをお勧めします。

私はこれを「増加する傾向」とは呼べなかっただろう。非常に大きなメソッドを小さなメソッドに分割すると、可読性が向上することを教えられました...うーん...約40年前。そして、設計時の同等の機能分解について教えられました。

私が理解していないのは、関数呼び出しを基本的にコメントとして使用するこの傾向であり、コードが再利用可能でない場合はコードを隠します。それがコード折りたたみの目的です。

いいえ。機能の分解とは、主に再利用可能なコンポーネント、関数、メソッドなどを作成することではありません。実際のところ、コードベースを理解しやすくする「バイトサイズのチャンク」に減らすことで、コードベースを理解しやすくします。

IMO、IDEコードの折りたたみでは同じことを実現できません。コードの折りたたみでは、通常、折りたたまれたコードが他のコードに及ぼす影響は考慮されません。たとえば、次のようになります。

    int a = 1;
    if (something()) {
        a = a + 1;
    }
    print(a);

IDEがifステートメントの本体を折りたたむことを決定した場合、プログラマはaが初期値から変更される可能性があることに気付かない可能性があります。何を折りたたむかを決めるのはプログラマ次第でした、それから彼/彼女は決定するためにコードを理解しなければなりません...これはプロセスを循環的にします。

対照的に、コードが次のように記述されている場合:

   int a = 1;
   a = someMethod(a);
   print(a);

どこ

   function someMethod(a):
       return something() ? a + 1 : a;

同じ「驚き」はありません。

(明らかにこれは非常に非現実的な例です...しかし、コードの折りたたみの問題を示しています。)

21
Stephen C

プログラムの機能を理解するために、コードの1行ごとを読む必要はないと思います。関数に適切な名前が付けられている場合、期待どおりの結果が得られない場合を除いて、なぜ内容を確認するのでしょうか。

小さい関数を作成するもう1つの利点は、単体テストです。テストに合格し、詳細を忘れる小さな関数を記述します。デバッグとトラブルシューティングの鍵は、レビューとコードの変更をできるだけ少なくすることです。

GOTOのアナロジーは問題の「ジャンプ先」の部分を説明するかもしれませんが、関数がどこに戻るかを正確に知っています。それは大きな違いです。

プログラミングの多くのことに慣れるのはとても簡単です。 GUIを使用した設計は、最終的に基礎となるコードを確認する必要があり、完全に失われる場合には役に立ちません。

10
JeffO

Robert C. Martinの本 Clean Code で詳しく説明されているように、短い関数を書くことにはいくつかの直接的な利点と多くの間接的な利点があります。オフメモリ:

  • 短い関数は一目で読みやすくなるため、読みやすさが向上します
  • 後でこれらの小さな関数を再利用しやすくします(前提条件としてこの可能性を排除したことは知っていますが...)

間接的な利点は軍団であり、「大きな関数を小さな関数にリファクタリングできない場合に得られないもの」と言い換えることができます。

  • 副作用を最小限に抑えます(長い関数を使用している場合、後で変数を数百行変更することは完全に可能です-頭の中で追跡するのは非常に困難です)
  • どの変数が使用されているか、そして何のために-入力、出力-を明確にし、依存関係の追跡を容易にします
  • 意味のある関数名を使用することで、明確なコードが奨励されます(関数が何をしているかを簡単に説明できない場合...)
  • うまくいけば、大きな関数をほとんど機能しない小さな関数にリファクタリングすると、共通のパターンに気付くでしょう。たとえば、再利用できなかったと思っていた関数ができるようになります。または、少なくとも一般的なアルゴリズムに置き換えられます。

これらの理由はよく理解されており、本や関連する回答を通してそれらすべてについて読むことができます。しかし、ここで強調しておきたい2番目の段落について説明します。

短い関数は素晴らしいですが、私たちのIDEはそれらを難しくするように設計されています!

コードの折りたたみについて言及したことで、これをほのめかしました。なぜこれができるのですか?

SomeLongFunction(doohickey) {
    [+] /// Frobnicate the doohickey
    foreach (d : doohickeys) {
        [+] /// Reticulate the doohickey
    }
}

... [+]sを展開することにより、実際のコードをインラインで展開して表示しますが、これはできません。

SomeLongFunction(doohickey) {
    splines = FrobnicateTheDoohickey(doohickey);
    foreach (d : doohickeys) {
        ReticulateTheDoohickey(d);
    }
}

...そして関数呼び出しを「拡張」しますか? IDEにそれらの関数定義に移動するように依頼することができますが、それがどこにあるかに応じて、新しいファイルの新しいクラス、または同じファイルの新しい場所に移動することができます、呼び出し側のコンテキストを失います!コンテキスト間を簡単に移動できるツールはありますが、コードの折りたたみを使用した場合のように、すべてのコードをインラインで表示できないのはなぜですか?これは完全に逆です; IDEコードの折りたたみなどを行う機能を導入することで、悪いコードを書くように勧めないでください。

コードをインラインで表示できることの利点は次のとおりです(折りたたみでも、他の関数の呼び出しでも)。

  • 任意の粒度でコードを表示するという選択肢が与えられます-BFS、DFS、またはその間の任意の組み合わせをトラバースできます。
  • コードを1つのコンテキストで使用すると、(gasp)を上または下にスクロールして、以前または後で実行されるコードを簡単に見つけることができます
  • 特定の折りたたみセクションを展開することで(これも私の選択です)、コードの複雑さをよりよく理解できるので、コードを簡単に理解できます。一度に1つの機能の表示に制限されているため、複雑さを隠すことでこれを妨げています。

もう1つの見方は、より大きなまたは複数のモニターを持つことに似ています-一度に多くを見ることができるため、多くの場合「必要」ではありませんが、良いコードには短い行があり、短い機能。ちなみに、 複数のモニターは生産性を向上させることが証明されています 。たくさん。

では、なぜ私たちのエディタやIDEがこの種のインライン関数呼び出しの拡張を許可しないのですか? (もしあれば、教えてください!)わかりません。短い関数には多大な利点があることは明らかです。また、たくさんのコードを一緒に表示できることも有益であることも私には明らかです。なぜ両方できないのですか?

7
congusbongus

まず、@ Stephen Cが言ったように、これは新しい考えではありません。それは古く、それがずっと昔からある理由はそれが機能するためです。

第二に、機能する理由は、関数型プログラミングがホットな話題になりつつあるのと同じ理由です。これにより、プログラムの推論が容易になります。物理的にトレースするのは難しくなりますが(ツールの機会のように聞こえます)、個々のピースを簡単に調べることができます。

ダースのパラメーターを取り、ブール値を返す関数を実行します-すぐに、2つの値のいずれかを返すことがわかっています。トレースする必要はありません。7つのオブジェクトと4つのユーティリティ関数で16のメソッドを呼び出すことを確認してください。trueまたはfalseを返すため、これらはすべて無関係です。

ここで、戻り値が関数の分割にどのように対応するかを尋ねているかもしれませんが、実際にはそれらは同じものです-ダースのパラメーターを取り、ブール値を返す関数は、一連の計算があることを示すもう1つの方法です最高の結果が1つになり、他の場所で使用されます。 void関数は、論理的につながっており、1つとして処理できる一連の操作です。

大規模な関数を分割すると、たとえどの部分も再利用できなくても、適切な抽象化レベルでその部分をよりよく心に留めることができます。 F1、F2などを10回呼び出すだけで構成されるFという名前の関数は、Fが何をしているのかを理解しようとしている間だけは推論するのが難しいです-良い名前は分数で測定するのが最も良い時間に短縮されます秒の。しかし、命名規則が不適切であっても、4に続く6に続くステップに焦点を当てている間、プログラムの残りの部分を無視しやすくなります。さらに、ステップに分解すると、オブジェクトの状態が変更され、グローバルが乱用、またはF1がF10に提供する必要のある情報が返され、ローカル変数に保持されます...すべての場合で、小さな部分で変更された場所を追跡する方が簡単になります。

5
jmoreno

関数とメソッドをより小さなものに分割することは、再利用性のためだけではありません。それは分離されたコードを奨励するので、それは良い一般的な設計慣行です。分離されたコードは、読み取りと保守が容易です。

これはどういう意味ですか?

100行のコード関数がある場合。これは、ユーザーがドメインオブジェクトに変更を加えた場合の処理​​を処理します。この関数は長く、100の個別の処理を実行しています。

これを2つの関数に分割すると、最初の関数は51の個別の処理を実行し、2番目の関数は50の個別の処理を実行します。ただし、2番目の関数はおそらく他の50の副作用を引き起こす可能性がある1つのことだけを行っています。メイン関数にバグがある場合、おそらくそれを新しいメソッド呼び出しまたは他の50行のコードのいずれかに分離できます。単純なリファクタリングの1つで、バグを見つけるために必要なコードの量を半分に減らしました。

業界として、グローバル変数やシステム全体の依存関係などを言語やコードから削除するために、ますます努力を重ねてきました。ある程度までは、手続き型コードのすべてのチャンクを独自のグローバルスコープと見なすことができます。このように組み立てると、このスコープのサイズをできるだけ小さくしたいと思うことが非常に簡単になります。 1つのことを行う小さなメソッドを使用すると、これが非常に役立ちます。 just可読性の問題ではありません。

2
Stephen

このアプローチは、主にコードを読みやすくするために行われていると思います。そして、はい、私は同意します。この道を行き過ぎる可能性があります。すべての関数には2〜3行しか含まれておらず、その結果、主な関数は読みやすくありません。これをうまくやることは、何よりも芸術です。

私の個人的なガイドは、メイン関数の長さが10行以上短縮される場合、分離された行がすべて論理的に同じタスク、およびそのタスクにわかりやすい名前を付けることができる場合(そうしないと、メイン関数が読みにくくなるため)。

完全に無意味にバストアウトされたマイクロアイデアの不協和音を読んで理解するよりも、物事をバストアウトして必要に応じて再利用する方が簡単です。

常に明確にするために誤りを犯しますが、それが何の役にも立たない場合は、未使用のものを1つの関数内に残してください。あなたはその状態ではそれを必要とするつもりはありません。

1
Erik Reppen

私は学校で教えられた若いプログラマーと協力して、コードは正しく因数分解するための機能は短いものの、経験が不足している必要があると述べています。

私はかなり反対のことをしていることがよくあります。コードを読みやすくするために、呼び出しサイトでこれらの関数またはメソッドをインライン化します。

よく構造化されたコードは通常簡潔である必要がありますが、特定の関数またはメソッドの画面よりも大きくないことが重要です関数が重要です。

私がやっていることは、コードの臭いのルールに従ってコードを整理することです。

長い関数はよく知られているコードの匂いですが、それだけではなく、最も重要ではありません。

たとえば、ランダムにコードブロックを取得して、多くの経験の浅いプログラマーと同じようにメソッドに変更する場合は、long functionslarge classと交換するだけです。さらに悪いことに、これはメソッドがメソッドパラメータを介してデータの一部を受け取り、オブジェクトインスタンスから一部を受け取る可能性があります。そのにおい(一時フィールド ...グローバルを使用したオブジェクト指向プログラミングの方法)を回避することは、短いメソッドよりもはるかに重要です。

また、関数またはメソッドは、明確に識別可能なタスクを1つだけ実行する必要があります。関数がタスクの半分を実行し、数行後にタスクの残りの半分を実行する別の関数に渡されるパラメーターを返すことがよくあります(これはData Clumps臭いの場合があります)。または、唯一のタスクが別のタスクを呼び出すことであり、基本的に何もしない関数(Lazyメソッド)。これも恐ろしいことです。通常、ローカル変数の使用を確認することで簡単に検出できます(これらのローカル変数が前のケースのようにオブジェクトインスタンスに配置されていない場合は、一時フィールドにおい)。

別の一般的なにおいは、ブール値を関数に渡すことです。多くの場合、提供されたブール値に応じて、タスクを実行する関数を隠します。おそらく、それを2つの関数に分割する方がよいでしょう(私は統合失調症の方法においを呼んでいます)。

最初に機能の選択が常に悪いわけではないという点で興味深いことですが、時間の経過に伴うコードの進化は、しばしば上記の問題につながります。

なぜそれが起こるのかについての私の説明はプログラマの心理学にあり、おそらく多くのメソッドや関数の間でコードを分割することの主な欠点です。

関数/メソッドで簡単に目をくらまして、基本的にこれらのメソッド内にコードがあることを忘れることができます。一部の(経験の浅い)プログラマーは、メソッドを作成すると、あたかもそれが新しいライブラリー呼び出しであるかのように当然のことと見なします。彼らは、サードパーティのライブラリー呼び出しで行うような変更を許可しません。 (ユーザー定義クラスでも同じ問題が発生します)。

驚くほど十分ですが、逆も機能します。一部の経験の浅いプログラマーは、コードの初期値が短い場合、論理的に必要なメソッドまたはクラスを作成しません(一部のパラメーターを初期値に設定して関数をカリー化する一般的なケース)!!!

あなたがそれに気づいていないと慎重である場合、これは本当に悪くなる可能性があります。

要約すると、私が言っていることは、多くのメソッド/関数/クラス間でコードを分割することは、何をしているのかを知っている場合は本当に良いことであり、知らない場合は実際の問題です。

このため、私はこの特定のにおいを個人的なリストでかなり低くしました。

残念ながら、ネット上のコードの匂いリストのリストの先頭に近いことがよくあります。それは、人間と自動化されたツールの両方で、それが検出しやすい匂いの1つだからだと思います。

1
kriss

関数の明確に定義された識別子は、その関数に行くことなく、関数についての幅広い知識を提供する必要があります。再利用可能かどうかに関係なく、関数はdivide and conquerの原則に従うことを意図しており、ほとんどの場合、プログラマーは非メイン関数の設計/実装/コーディングを開始し、その後にのみ呼び出しが必要です。

0

それ以外の場合、コードが「適切なコード」である場合、コードブロックを目的に応じたコメントで展開するアクションと、デバッガを適切な名前の関数に続けるアクションとの間で、認識に実際の違いはありますか?

デバッガーを関数呼び出しに組み込むと、コンテキストが失われる可能性があります。ただし、これが、参照している関数の理解に大きな違いをもたらす場合は、コンテキストが重要であることを示しています。これは、呼び出されている関数の目的が明確でないため、コードは「適切なコード」ではありません。言い換えると、関数がその名前から何を行うかは、実際には信頼できません。これは別の問題です。

コードブロックが現在の関数の変数またはパラメーターを使用している可能性があり、コードブロックを拡張しないと確実にわかりません。コメントはブロックの機能を説明しますが、関数呼び出しのパラメーターは、関数に渡される現在のコンテキストからの依存関係も示します。これは実際にはより少ないコードを見てより多くのコンテキストであり、これは良いことです(もちろん、コードが実際に優れていると想定しています)。

作業中のコンテキストにより、非折りたたみエディターの使用が強制される場合があります。その場合、大きなコードブロックは関数の全体的な構造を覆い隠す可能性があります。たとえば、関数がすべてのブロックが折りたたまれているが、カバーする単一の画面に収まる場合それらが展開された多くの画面。

良い中間点は、呼び出される小さな関数のコードを「インライン化」できるIDE /エディタを用意することです。これにより、コンテキストが複数のファイルに分散している場合でも、コンテキストを確認できます。

0