web-dev-qa-db-ja.com

関数の呼び出しが1回だけになるときに、別の関数を作成するのが適切なのはいつですか。

私たちはコーディング標準を設計しており、コードをクラス内の個別の関数に分割することが適切かどうか、それらの関数が一度だけ呼び出されるかどうかについて意見が分かれています。

例えば:

f1()
{
   f2();  
   f4();
}


f2()
{
    f3()
    // Logic here
}


f3()
{
   // Logic here
}

f4()
{
   // Logic here
}

対:

f1()
{
   // Logic here
   // Logic here
   // Logic here
}

個別の使い捨てサブ関数を使用して大きな関数を分割すると、読みやすくなると主張する人もいます。ただし、コードを初めて読むときは、ロジックチェーンをたどってシステム全体を最適化するのは面倒です。この種の関数レイアウトに通常適用されるルールはありますか?

他の質問とは異なり、許可されている場合だけでなく、単一呼び出し関数の許可された使用と許可されていない使用を区別するための最良の条件セットを求めていることに注意してください。

46
David

関数を分割する背後にある理論的根拠は、それらが呼び出される回数ではありません、それはそれらを小さく保ち、それらがいくつかの異なることをするのを防ぎます。

Bob Martin'sClean Code は、関数をいつ分割するかについての適切なガイドラインを示しています。

  • 関数は小さくなければなりません。どれくらい小さい?下記の箇条書きをご覧ください。
  • 関数は1つのことだけを実行する必要があります。

そのため、関数が複数の画面である場合は、それを分割してください。関数がいくつかのことを行う場合は、分割します。

関数が最終結果を目的とした順次ステップで構成されている場合は、たとえそれが比較的長くても、分割する必要はありません。しかし、関数がある条件、別の条件、そして論理条件で区切られたブロックなどの機能を実行する場合は、分割する必要があります。そのロジックの結果として、関数は通常小さくなければなりません。

f1()が認証を行う場合、f2()は入力を小さな部分に解析し、f3()が計算を行い、f4()がログを記録するか結果を保持する場合、それらのすべてが一度だけ呼び出される場合でも、明らかに分離する必要があります。

これにより、読みやすくなるという追加の利点に加えて、それらを個別にリファクタリングおよびテストできます

一方、すべての関数が次の場合:

a=a+1;
a=a/2;
a=a^2
b=0.0001;
c=a*b/c;
return c;

この場合、ステップのシーケンスが長い場合でも、分割する必要はありません。

92

ここでは関数の命名が非常に重要だと思います。

非常に詳細に分析された関数は、非常に自己文書化することができます。関数内の各論理プロセスが独自の関数に分割され、内部ロジックが最小限の場合、各ステートメントの動作は、関数の名前とそれらが取るパラメーターによって推論できます。

もちろん、欠点もあります。関数名です。コメントのように、そのような非常に具体的な関数は、多くの場合、関数が実際に行っていることと同期しなくなる可能性があります。 しかし同時に、適切な関数名を与えることで、それをharderにしてスコープのクリープを正当化します。サブ関数に、明らかに本来あるべき以上のことをさせることは、より困難になります。

だから私はこれをお勧めします。他の誰もそれを呼び出さなくても、コードのセクションが分割される可能性があると確信している場合は、次の質問を自問してみてください。「どのような名前を付けますか?」

その質問への回答に5秒以上かかる場合、または選択した関数名が明確に不透明である場合は、関数内の個別の論理単位ではない可能性が高くなります。または、少なくとも、論理ユニットが実際にそれを適切に分割するために何をしているのかについて、十分に確信が持てないこと。

しかし、大幅に分析された関数が遭遇する可能性がある追加の問題があります:バグ修正。

200行以上の関数内の論理エラーを追跡するのは困難です。しかし、それらの間の関係を思い出そうとしながら、10以上の個別の機能を通じてそれらを追跡しますか?それはさらに難しい場合があります。

ただし、名前によるセマンティックな自己文書化は重要な役割を果たします。各関数に論理名がある場合、リーフ関数の1つを検証するために必要なのは(単体テストのほかに)、関数が実際に実行するかどうかを確認することだけです。葉の機能は短く、集中する傾向があります。したがって、すべての個々のリーフ関数が本来あるべきことを実行する場合、考えられる唯一の問題は、誰かが間違ったものを渡したことです。

したがって、その場合、バグを修正することができます。

私は、論理ユニットに意味のある名前を割り当てることができるかどうかという問題に本当に帰着すると思います。もしそうなら、それはおそらく機能である可能性があります。

43
Nicol Bolas

テキストのブロックが何をしているかを説明するコメントを書く必要があると感じるときはいつでも、メソッドを抽出する機会を見つけました。

のではなく

//find eligible contestants
var eligible = contestants.Where(c=>c.Age >= 18)
eligible = eligible.Where(c=>c.Country == US)

試す

var eligible = FindEligible(contestants)
12
dss539

DRY-繰り返さないでください-バランスをとる必要があるseveral原則の1つにすぎません。

ここで頭に浮かぶ他のいくつかは命名です。カジュアルな読者にとってロジックが複雑ではない場合、名前が何をよりカプセル化したメソッド/関数に抽出するかwhyしていると、プログラムの可読性が向上します。

また、//logicが何行になるかに応じて、コードメソッド/関数の5〜10行未満を狙うことも有効です。

また、パラメーターを持つ関数は、APIとして機能し、パラメーターに適切な名前を付けて、コードロジックをより明確にすることができます。

また、そのような関数のコレクションが時間の経過とともにドームの有用なグループ化を明らかにすることにも気付くかもしれません。管理者は、その下で簡単に収集できます。

5
Michael Durrant

関数の分割に関するポイントは、すべて1つです。それは、単純さです。

コードの読者は、同時に7つ以上のことを念頭に置くことはできません。関数はそれを反映する必要があります。

  • 長すぎる関数を作成すると、関数内に7つ以上のものが含まれるため、それらを読み取ることができなくなります。

  • 大量の1行関数を作成すると、読者も関数のもつれに混乱します。ここで、それぞれの目的を理解するために、7つ以上の関数をメモリに保持する必要があります。

  • 一部の機能は長くてもシンプルです。典型的な例は、関数に多くのケースを持つ1つの大きなswitchステートメントが含まれている場合です。各ケースの処理が単純である限り、関数は長くなりません。

両方の極値( (メガモス と小さな関数のスープ))も同様に悪いので、両者の間でバランスを取る必要があります。私の経験では、Nice関数には約10行あります。一部の関数は1行で、20行を超えるものもあります。重要なことは、各関数は、その実装において等しく理解可能でありながら、容易に理解できる関数を提供することです。

それはすべて懸念の分離についてです。 (わかりました、それについてallではありません。これは単純化です)。

これは問題ありません:

function initializeUser(name, job, bye) {
    this.username = name;
    this.occupation = job;
    this.farewell = bye;
    this.gender = Gender.unspecified;
    this.species = Species.getSpeciesFromJob(this.occupation);
    ... etc in the same vein.
}

この関数は、1つの懸念事項に関係しています。指定された引数、デフォルト、外挿などからユーザーの初期プロパティを設定します。

これは問題ではありません。

function initializeUser(name, job, bye) {
    // Connect to internet if not already connected.
    modem.getInstance().ensureConnected();
    // Connect to user database
    userDb = connectToDb(USER_DB);
    // Validate that user does not yet exist.
    if (0 != userDb.db_exec("SELECT COUNT(*) FROM `users` where `name` = %d", name)) {
        throw new sadFace("User exists");
    }
    // Configure properties. Don't try to translate names.
    this.username = removeBadWords(name);
    this.occupation = removeBadWords(translate(job));
    this.farewell = removeBadWords(translate(bye));
    this.gender = Gender.unspecified;
    this.species = Species.getSpeciesFromJob(this.occupation);
    userDb.db_exec("INSERT INTO `users` set `name` = %s", this.username);
    // Disconnect from the DB.
    userDb.disconnect();
}

懸念の分離は、これを複数の懸念として処理する必要があることを示唆しています。 DBの処理、ユーザーの存在の検証、プロパティの設定。これらはそれぞれユニットとして非常に簡単にテストできますが、1つの方法ですべてをテストすると、非常に複雑なユニットテストセットが作成されます。空で無効な役職、およびユーザーの作成を2回処理する方法(答え:悪い、バグがある)。

問題の一部は、それがどれほど高レベルであるかという点であらゆるところに行き渡っているということです。低レベルのネットワーキングとDBのものはここでは場所がありません。それは懸念の分離の一部です。他の部分は、何か他のものの懸念であるはずのものは、代わりにinit関数の懸念です。たとえば、不適切な言語フィルターを翻訳するか適用するかは、設定​​されるフィールドの問題としてより理にかなっています。

3
Dewi Morgan

// Logic Hereが何であるかに大きく依存します。

ワンライナーの場合、おそらく機能分解は必要ありません。

一方で、それが数行または数行のコードである場合は、別の関数に入れて適切に名前を付けることをお勧めします(f1,f2,f3はここでマスターを渡しません)。

これはすべて、平均して人間の脳に関係しています。大量のデータを一目で処理するには、あまり効率的ではありません。ある意味、どのデータでもかまいません。複数行機能、混雑した交差点、1000ピースのパズルです。

コードメンテナーの頭脳の友となり、エントリポイントの断片を切り詰めます。誰が知っているか、そのコードのメンテナは数か月後のあなたかもしれません。

普及しているコーディングの原則によると、「正しい」答えは、大きな関数を小さく、読みやすく、テスト可能にテストし、テスト済みの関数を自己文書化することです。名前。

とはいえ、「コードの行」に関して「大」を定義することは、恣意的で独断的であり、退屈であるように思えるかもしれません。しかし、恐れるな!コード行制限の背後にある主な目的が可読性とテスト容易性であることを認識すると、特定の関数に適切な行制限を簡単に見つけることができます! (そして内部的に関連のあるソフト制限の基盤の構築を開始します。)

巨石機能を許可し、関数をfirst記号で行をより小さな名前の付いた関数に抽出することをチームとして同意する全体として読むのが難しいか、サブセットの正確さが不確かな場合

...そして、最初の実装時に全員が正直で、誰も200を超えるIQを宣伝していない場合、他の誰かが自分のコードを見る前に、理解可能性とテスト容易性の限界を特定できることがよくあります。

1
svidgen