関数が受け入れるパラメータの数に関するガイドラインはありますか?


114

私が使用しているいくつかの関数には6つ以上のパラメータがあることに気づきましたが、私が使用するほとんどのライブラリでは、3つ以上かかる関数を見つけることはまれです。

多くの場合、これらの追加パラメーターの多くは、関数の動作を変更するためのバイナリオプションです。これらの無数のパラメーター化された関数のいくつかはおそらくリファクタリングされるべきだと思います。数が多すぎる場合のガイドラインはありますか?


4
@Ominus:アイデアは、クラスに集中したいということです。通常、フォーカスされたクラスにはそれほど多くの依存関係/属性がないため、コンストラクターへのパラメーターが少なくなります。この時点で人々が投げかける話題の言葉は、高い結束性単一責任の原則です。これらに違反していないと感じ、まだ多くのパラメータが必要な場合は、Builderパターンの使用を検討してください。
c_maker

2
12のパラメーターをとるMPI_Sendrecv()の例には絶対従わないでください!
-chrisaycock

6
現在取り組んでいるプロジェクトでは、特定のフレームワークを使用しています。このフレームワークでは、10以上のパラメーターを持つメソッドが一般的です。いくつかの場所で27個のパラメーターを持つ特定のメソッドを呼び出しています。私はそれを見るたびに、私は少し内側で死にます。
-perp

3
関数の動作を変更するためにブールスイッチを追加しないでください。代わりに関数を分割してください。一般的な動作を新しい関数に分割します。
ケビンクライン

2
@Ominus何?パラメーターは10個だけですか?それは何でもありません、もっともっと必要です。:D
maaartinus

回答:


106

ガイドラインを見たことがありませんが、私の経験では、3つまたは4つ以上のパラメーターを取る関数は、2つの問題のいずれかを示しています。

  1. 関数がやりすぎです。いくつかの小さな関数に分割し、各関数のパラメーターセットを小さくする必要があります。
  2. そこに隠れている別のオブジェクトがあります。これらのパラメーターを含む別のオブジェクトまたはデータ構造を作成する必要がある場合があります。詳細については、パラメータオブジェクトパターンに関するこの記事を参照してください。

これ以上の情報がなければ、あなたが見ているものを伝えることは困難です。関数を現在の関数に渡されているフラグに応じて親から呼び出される小さな関数に分割する必要があるリファクタリングがチャンスです。

これを行うことで得られるいくつかの良い利益があります:

  • コードを読みやすくします。個人的にはif、1つのメソッドですべてを実行する構造よりも、記述的な名前で多くのメソッドを呼び出す構造で構成される「ルールリスト」を読む方がはるかに簡単です。
  • ユニットテスト可能です。問題を、個々に非常に単純ないくつかの小さなタスクに分割しました。単体テストコレクションは、マスターメソッドを通るパスをチェックする動作テストスイートと、個々の手順ごとの小さなテストのコレクションで構成されます。

5
パラメータの抽象化は設計パターンに変わりましたか?3つのパラメータークラスがある場合はどうなりますか。パラメーターのさまざまな可能な組み合わせを処理するために、さらに9つのメソッドオーバーロードを追加しますか?これは厄介なO(n ^ 2)パラメーター宣言の問題のように聞こえます。ああ、Java / C#で継承できるクラスは1つだけなので、実際に機能させるには、より多くのbiolerplate(サブクラス化が必要な場合があります)が必要になります。すみません、納得できません。言語が複雑さを支持して提供するかもしれない、より表現力のあるアプローチを無視することは、間違っていると感じるだけです。
エヴァンプライス

パターンオブジェクトパターンを使用して変数をオブジェクトインスタンスにパッケージ化し、それをパラメーターとして渡す場合を除きます。これはパッケージングには有効ですが、メソッド定義を単純化するためだけに、異なる変数のクラスを作成したくなるかもしれません。
エヴァンプレイス

@EvanPlaice複数のパラメーターがある場合は常にそのパターンを使用する必要があると言っているわけではありません-最初のリストよりもさらに厄介になることは絶対に正しいです。本当に多数のパラメーターが必要な場合がありますが、それらをオブジェクトにラップするだけでは機能しません。私の回答で言及した2つのバケットのいずれにも該当しないエンタープライズ開発のケースにまだ出会っていません。つまり、1つが存在しないと言っているわけではありません。
マイケルK

@MichaelK使用したことがない場合は、「オブジェクト初期化子」をググリングしてみてください。定義ボイラープレートを大幅に削減する非常に斬新なアプローチです。理論的には、クラスコンストラクター、パラメーター、およびオーバーロードをすべて一度に排除できます。しかし実際には、通常、1つの共通のコンストラクタを維持し、残りのあいまい/ニッチプロパティについては「オブジェクト初期化子」構文に依存することをお勧めします。私見、それはあなたが静的に型付けされた言語で動的に型付けされた言語の表現力に最も近いです。
エヴァンプライス

@Evain Plaice:いつから動的に型付けされた言語は表現力豊かになりましたか?
ThomasX

41

「Clean Code:A Handbook of Agile Software Craftsmanship」によると、ゼロが理想であり、1つまたは2つが許容され、3つは特別な場合、4つ以上は決して許されません!

著者の言葉:

関数の引数の理想的な数はゼロ(niladic)です。次に1つ(単項)、2つ(2項)が続きます。可能な場合、3つの引数(3項)を避ける必要があります。3つ以上(ポリアディック)には非常に特別な正当化が必要です。

この本には、パラメータが大規模に議論されている関数についてのみ説明している章があるので、この本は必要なパラメータの量の良いガイドラインになると思います。

私の意見では、何が起こっているかがより明確だと思うので、1つのパラメーターは誰よりも優れています。

例として、私の意見では、メソッドが処理しているものがより明確であるため、2番目の選択の方が優れています。

LangDetector detector = new LangDetector(someText);
//lots of lines
String language = detector.detectLanguage();

LangDetector detector = new LangDetector();
//lots of lines
String language = detector.detectLanguage(someText);

多くのパラメーターについて、これは、いくつかの変数を単一のオブジェクトにグループ化できることを示す兆候である場合があります。これらの動作のそれぞれを別の関数でリファクタリングする方が適切です。


8
「特別な場合は3つ、4つ以上は、決して!」BS。Matrix.Create(x1、x2、x3、x4、x5、x6、x7、x8、x9)はどうですか??
ルカシュマドン

71
ゼロは理想的ですか?関数はどのようにして情報を取得しますか?グローバル/インスタンス/静的/なんでも変数?ユック。
ピーターC

9
それは悪い例です。答えは明らかに:String language = detectLanguage(someText);です。どちらの場合でも、まったく同じ数の引数を渡しましたが、言語が貧弱であるために関数の実行を2つに分割しただけです。
マチューM.

8
@lukas、配列や(gasp!)リストなどの派手なコンストラクトをサポートする言語Matrix.Create(input);inputは、たとえば.NETはIEnumerable<SomeAppropriateType>どこにありますか?あなたは10個の要素の代わりに9を保持するマトリックスを作成したいときにも、別のオーバーロードを必要としないその方法
からCVn

9
「理想」としてのゼロ引数は不器用であり、Clean Codeが過大評価されていると思う理由の1つです。
user949300

24

アプリケーションのドメインクラスが正しく設計されている場合、関数に渡すパラメーターの数は自動的に削減されます。クラスは仕事をする方法を知っており、仕事をするのに十分なデータがあるためです。

たとえば、3年生のクラスに課題を完了するよう依頼するマネージャークラスがあるとします。

正しくモデリングすれば、

3rdGradeClass.finishHomework(int lessonId) {
    result = students.assignHomework(lessonId, dueDate);
    teacher.verifyHomeWork(result);
}

これは簡単です。

正しいモデルがない場合、メソッドは次のようになります

Manager.finishHomework(grade, students, lessonId, teacher, ...) {
    // This is not good.
}

正しい関数は独自のクラスに委任され(単一の責任)、作業を行うのに十分なデータがあるため、正しいモデルはメソッド呼び出し間の関数パラメーターを常に減らします。

パラメーターの数が増えるのを見るたびに、モデルをチェックして、アプリケーションモデルを正しく設計したかどうかを確認します。

ただし、いくつかの例外があります。転送オブジェクトまたは構成オブジェクトを作成する必要がある場合、ビルダーパターンを使用して、小さな構成オブジェクトを最初に作成してから、大きな構成オブジェクトを作成します。


16

他の答えが取り上げない側面の1つはパフォーマンスです。

十分に低レベルの言語(C、C ++、アセンブリ)でプログラミングしている場合、特に関数が大量に呼び出される場合、一部のアーキテクチャでは多数のパラメーターがパフォーマンスに非常に悪影響を与える可能性があります。

たとえば、ARMで関数呼び出しが行われる場合、最初の4つの引数はレジスタのレジスタr0に配置されr3、残りの引数はスタックにプッシュする必要があります。引数の数を5未満に保つと、重要な機能に大きな違いが生じる可能性があります。

非常に頻繁に呼び出される関数、プログラムは各呼び出しは、パフォーマンスに影響することができます(前の引数を設定する必要があることも、事実のためr0r3呼び出された関数で上書きすることができるが、次の呼び出しの前に交換する必要があります)ので、その点で引数はゼロが最適です。

更新:

KjMagは、インライン化の興味深いトピックを取り上げます。インライン化は、コンパイラが純粋なアセンブリで記述する場合と同じ最適化を実行できるため、いくつかの点でこれを軽減します。つまり、コンパイラーは、呼び出された関数がどのパラメーターと変数を使用しているかを確認し、レジスターの使用を最適化して、スタックの読み取り/書き込みを最小限に抑えることができます。

ただし、インライン化にはいくつかの問題があります。

  1. インライン化すると、同じコードが複数の場所から呼び出された場合にバイナリ形式で複製されるため、コンパイルされたバイナリが大きくなります。これは、Iキャッシュの使用に関しては有害です。
  2. コンパイラは通常、特定のレベルまでのインライン化のみを許可します(3ステップIIRC?)。インライン関数からインライン関数からインライン関数を呼び出すことを想像してください。inlineすべての場合に必須として扱われた場合、バイナリ成長は爆発するでしょう。
  3. コンパイラは、完全に無視するinlineか、実際にエラーが発生したときに実際にエラーを表示します。

多数のパラメーターを渡すことがパフォーマンスの観点から良いか悪いかは、選択肢に依存します。メソッドが数十個の情報を必要とし、そのうちの11個に対して同じ値を使用して何百回も呼び出す場合、メソッドが配列を取るのは、数十個のパラメーターを取るよりも高速です。一方、すべての呼び出しが12個の値の一意のセットを必要とする場合、各呼び出しの配列の作成と入力は、単純に値を直接渡すよりも簡単に遅くなる可能性があります。
supercat 14

インライン化はこの問題を解決しませんか?
KjMag

@KjMag:はい、ある程度まで。しかし、コンパイラーに応じて多くの落とし穴があります。関数は通常、特定のレベルまでインライン化されます(インライン関数を呼び出すインライン関数を呼び出すインライン関数を呼び出す場合...)。関数が大きく、多くの場所から呼び出される場合、どこでもインライン化するとバイナリが大きくなり、Iキャッシュのミスが増える可能性があります。したがって、インライン化は役立ちますが、特効薬ではありません。(言うまでもなく、サポートしていない古い組み込みコンパイラがかなりありますinline。)
Leo

7

パラメータリストが5つ以上になったら、「コンテキスト」構造またはオブジェクトの定義を検討してください。

これは基本的に、いくつかの適切なデフォルトが設定されたすべてのオプションパラメータを保持する構造です。

C手続き型の世界では、単純な構造で十分です。Java、C ++では、単純なオブジェクトで十分です。オブジェクトの唯一の目的は「パブリック」に設定可能な値を保持することであるため、ゲッターやセッターを混乱させないでください。


関数のパラメーター構造がかなり複雑になり始めると、コンテキストオブジェクトが非常に便利になることがあります。私は最近使っについてブログしていた訪問者のようなパターンを持つオブジェクトコンテキストを
ルーカス・エデル

5

いいえ、標準のガイドラインはありません

しかし、多くのパラメータを持つ関数をより耐えられるものにすることができるいくつかのテクニックがあります。

list-if-argsパラメーター(args *)またはdictionary-of-argsパラメーター(kwargs **)を使用できます

たとえば、Pythonの場合:

// Example definition
def example_function(normalParam, args*, kwargs**):
  for i in args:
    print 'args' + i + ': ' + args[i] 
  for key in kwargs:
    print 'keyword: %s: %s' % (key, kwargs[key])
  somevar = kwargs.get('somevar','found')
  missingvar = kwargs.get('somevar','missing')
  print somevar
  print missingvar

// Example usage

    example_function('normal parameter', 'args1', args2, 
                      somevar='value', missingvar='novalue')

出力:

args1
args2
somevar:value
someothervar:novalue
value
missing

または、オブジェクトリテラル定義構文を使用できます

たとえば、AJAX GETリクエストを起動するJavaScript jQuery呼び出しは次のとおりです。

$.ajax({
  type: 'GET',
  url: 'http://someurl.com/feed',
  data: data,
  success: success(),
  error: error(),
  complete: complete(),
  dataType: 'jsonp'
});

jQueryのajaxクラスを見ると、設定できるプロパティがさらに多く(約30)あります。これは主に、ajax通信が非常に複雑だからです。幸いなことに、オブジェクトリテラル構文により作業が楽になります。


C#intellisenseはパラメータのアクティブなドキュメントを提供するため、オーバーロードされたメソッドの非常に複雑な配置を見るのは珍しいことではありません。

python / javascriptのような動的に型付けされた言語にはそのような機能がないため、キーワード引数とオブジェクトリテラル定義を見るのがより一般的です。

オブジェクトがインスタンス化されるときにどのプロパティが設定されているかを明示的に確認できるため、複雑なメソッドの管理にオブジェクトリテラル定義(C#でも)が好まれます。デフォルトの引数を処理するにはもう少し作業が必要になりますが、長期的にはコードはずっと読みやすくなります。オブジェクトリテラル定義を使用すると、ドキュメントへの依存を解消して、コードが一目で何をしているかを理解できます。

私見、オーバーロードされたメソッドは非常に過大評価されています。

注:覚えていれば、C#のオブジェクトリテラルコンストラクターで読み取り専用アクセス制御が機能するはずです。基本的に、コンストラクターでプロパティを設定するのと同じように機能します。


動的に型指定された(python)および/または機能/プロトタイプjavaScriptベースの言語で自明でないコードを記述したことがない場合は、試してみることを強くお勧めします。それは啓発的な経験になる可能性があります。

最初に、関数/メソッドの初期化へのすべてのアプローチを行うためのパラメーターへの依存を破ることは怖いかもしれませんが、不必要な複雑さを追加することなく、コードでこれ以上のことを行うことを学びます。

更新:

おそらく静的に型付けされた言語での使用を示すために例を提供すべきでしたが、現在は静的に型付けされたコンテキストで考えているわけではありません。基本的に、動的に型付けされたコンテキストであまりにも多くの作業を行って、突然元に戻りました。

知っているのは、オブジェクトリテラル定義構文が静的型付け言語(少なくともC#とJava)で完全に可能であることです。静的に型付けされた言語では、それらは「オブジェクト初期化子」と呼ばれます。JavaおよびC#での使用方法を示すリンクを次に示します。


3
主に、個々のパラメーターの自己文書化値を失うため、この方法が好きかどうかはわかりません。類似アイテムのリストの場合、これは完全に理にかなっています(たとえば、文字列のリストを取得して連結するメソッド)が、任意のパラメーターセットの場合、これは長いメソッド呼び出しよりも劣ります。
マイケルK

@MichaelKオブジェクト初期化子をもう一度見てください。従来のメソッド/関数パラメーターで暗黙的に定義される方法とは対照的に、プロパティを明示的に定義できます。これをmsdn.microsoft.com/en-us/library/bb397680.aspxで読んで、私が何を言っているかを確認してください
エヴァンプライス

3
パラメーターリストを処理するためだけに新しい型を作成すると、不必要な複雑さの定義とまったく同じように聞こえます。とにかく、これは質問に答えません。
テラスティン

@Telastyn何言ってるの?新しい型は作成されませんでした。オブジェクトリテラル構文を使用してプロパティを直接宣言します。匿名オブジェクトを定義するようなものですが、メソッドはそれをkey = valueパラメーターのグループ化として解釈します。あなたが見ているのはメソッドのインスタンス化です(オブジェクトをカプセル化するパラメーターではありません)。あなたの牛肉がパラメータのパッケージングを使用している場合、それがまさにそれであるので、他の質問の1つで言及されたパラメータオブジェクトパターンを見てください。
エヴァンプライス

@EvanPlaice-静的プログラミング言語は、一般に、パラメータオブジェクトパターンを許可するために(多くの場合新しい)宣言された型を必要とすることを除きます。
テラスティン

3

個人的には、私のコードの匂いアラームがトリガーされるのは2つ以上です。関数を操作(つまり、入力から出力への変換)と見なす場合、1つの操作で3つ以上のパラメーターが使用されることはまれです。手順(目標を達成するための一連のステップ)はより多くの入力を必要とし、時には最良のアプローチですが、ほとんどの言語では最近では標準ではありません。

しかし、繰り返しますが、それはルールというよりもガイドラインです。異常な状況や使いやすさのために、3つ以上のパラメーターを取る関数がよくあります。


2

Evan Plaiceが言っているように、私は可能な限り連想配列(またはあなたの言語の同等のデータ構造)を関数に単純に渡すことの大ファンです。

したがって、(たとえば)これの代わりに:

<?php

createBlogPost('the title', 'the summary', 'the author', 'the date of publication, 'the keywords', 'the category', 'etc');

?>

をやる:

<?php

// create a hash of post data
$post_data = array(
  'title'    => 'the title',
  'summary'  => 'the summary',
  'author'   => 'the author',
  'pubdate'  => 'the publication date',
  'keywords' => 'the keywords',
  'category' => 'the category',
  'etc'      => 'etc',
);

// and pass it to the appropriate function
createBlogPost($post_data);

?>

Wordpressはこの方法で多くのことを行いますが、うまく機能すると思います。(上記の私のサンプルコードは架空のものであり、それ自体はWordpressの例ではありません。)

この手法を使用すると、多くのデータを関数に簡単に渡すことができますが、各データを渡す順序を覚えておく必要はありません。

また、リファクタリングするときが来ると、この手法に感謝します-関数の引数の順序を潜在的に変更する代わりに(まだ別の引数を渡す必要があることに気づいたときなど)、関数のパラメーターを変更する必要はありませんすべてのリスト。

これにより、関数定義を書き直す必要がなくなるだけでなく、関数が呼び出されるたびに引数の順序を変更する必要がなくなります。それは大きな勝利です。


この投稿を見ると、ハッシュパスアプローチの別の利点がわかります。最初のコード例は非常に長いためスクロールバーが生成されますが、2番目のコード例はページにきれいに収まります。コードエディターでも同じことが当てはまります。
クリスアレンレーン

0

以前の回答では、関数のパラメーターが少なければ少ないほど良いと述べた信頼できる著者に言及しました。答えは理由を説明しませんでしたが、本はそれを説明します、そして、ここにあなたがこの哲学を採用する必要がある最も説得力のある理由の2つがあり、私は個人的に同意します:

  • パラメーターは、関数の抽象化レベルとは異なる抽象化レベルに属します。これは、コードの読者が関数のパラメーターの性質と目的について考える必要があることを意味します。この思考は、名前の対応や機能の目的よりも「低いレベル」です。

  • 関数のパラメーターをできるだけ少なくする2番目の理由はテストです。たとえば、10個のパラメーターを持つ関数がある場合、たとえばユニットのすべてのテストケースをカバーする必要があるパラメーターの組み合わせの数を考えます。テスト。パラメータが少ない=テストが少ない。


0

Robert Martinの「Clean Code:A Handbook of Agile Software Craftsmanship」で、関数引数の理想的な数がゼロであるというアドバイスについてもう少しコンテキストを提供するために、著者は次のことを彼のポイントの1つとして述べています。

引数は難しいです。彼らは多くの概念的な力を取ります。そのため、例からそれらのほとんどすべてを取り除きました。たとえば、例を見てみましょうStringBuffer。インスタンス変数にするのではなく、引数として渡すこともできますが、読者はそれを見るたびに解釈しなければなりません。モジュールによって語られたストーリーを読んでいるときは、includeSetupPage() を理解するよりも簡単ですincludeSetupPageInto(newPageContent)。引数は、関数名の抽象化のレベルが異なりStringBuffer、その時点では特に重要ではない詳細(つまり)を知るように強制します。

includeSetupPage()上記の彼の例については、章の最後にある彼のリファクタリングされた「クリーンなコード」の小さな断片です。

// *** NOTE: Commments are mine, not the author's ***
//
// Java example
public class SetupTeardownIncluder {
    private StringBuffer newPageContent;

    // [...] (skipped over 4 other instance variables and many very small functions)

    // this is the zero-argument function in the example,
    // which calls a method that eventually uses the StringBuffer instance variable
    private void includeSetupPage() throws Exception {
        include("SetUp", "-setup");
    }

    private void include(String pageName, String arg) throws Exception {
        WikiPage inheritedPage = findInheritedPage(pageName);
        if (inheritedPage != null) {
            String pagePathName = getPathNameForPage(inheritedPage);
            buildIncludeDirective(pagePathName, arg);
        }
    }

    private void buildIncludeDirective(String pagePathName, String arg) {
        newPageContent
            .append("\n!include ")
            .append(arg)
            .append(" .")
            .append(pagePathName)
            .append("\n");
    }
}

著者の「思考の学校」は、小さなクラス、少ない(理想的には0)関数引数の数、および非常に小さな関数について論じています。私も彼に完全に同意していませんが、私はそれが思考を刺激するものであるとわかりました。また、上記の彼の小さなコードスニペットにもゼロ以外の引数関数があるため、コンテキストに依存すると思います。

(そして、他の人が指摘したように、彼はまた、より多くの引数がテストの観点からそれを難し​​くすると主張します。しかし、ここで主に上記の例とゼロ関数引数の彼の理論を強調したかったです。)


-2

理想的にはゼロ。1つまたは2つは問題ありませんが、特定の場合は3つです。
通常、4つ以上は悪い習慣です。

他の人が指摘した単一の責任原則だけでなく、テストとデバッグの観点から考えることもできます。

1つのパラメーターがある場合、その値を知って、それらをテストし、それらのエラーを見つけるのは、「1つの要因しかないため、比較的簡単です。係数を大きくすると、全体の複雑さが急速に増加します。抽象の例:

「この天気で何を着るか」プログラムを検討してください。1つの入力-温度で何ができるかを検討してください。あなたが想像できるように、何を着るべきかの結果は、その1つの要因に基づいて非常に簡単です。次に、実際に温度、湿度、露点、降水量などが渡された場合にプログラムが何をする/できる/すべきかを考えます。


12
関数のパラメーターがゼロの場合、定数値を返す(状況によっては有用ですが、制限する)か、明示的にした方がよい隠し状態を使用しています。(オブジェクト指向メソッドの呼び出しでは、コンテキストオブジェクトは問題を引き起こさないように十分に明示的です。)
ドナルフェローズ

4
-1ソースを引用しないため
ジョシュアドレイク

理想的にはすべての関数がパラメーターをとらないと真剣に言っていますか?それともこれは誇張ですか?
GreenAsJade

1
informit.com/articles/article.aspx?p=1375308にあるボブおじさんの議論を参照してください。下部には「関数には少数の引数が必要です。引数はなく1、2、3が続くのが最適です」 。3つ以上は非常に疑わしく、偏見を持って避けるべきです。」
マイケルデュラント14

ソースを提供しました。それ以来面白いコメントはありません。多くの人がボブおじさんとクリーンコードをガイドラインと見なしているため、「ガイドライン」の部分にも答えようとしました。興味深いことに、(現在)非常に賛成のトップの回答は、ガイドラインを認識していないと言っています。ボブおじさんは権威を与えるつもりはなかったが、それはいくぶんそうであり、この答えは少なくとも質問の詳細に対する答えになろうとしている。
マイケルデュラント
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.