単一の参照を持つプライベートメソッドは悪いスタイルですか?


139

通常、プライベートメソッドを使用して、クラス内の複数の場所で再利用される機能をカプセル化します。しかし、私は時々、それぞれが独自のプライベートメソッドの小さなステップに分割できる大きなパブリックメソッドを持っています。これにより、パブリックメソッドが短くなりますが、メソッドを読み取る人に別のプライベートメソッドにジャンプさせると読みやすさが損なわれるのではないかと心配しています。

これに関してコンセンサスはありますか?長いパブリックメソッドを使用する方が良いでしょうか、それとも各ピースが再利用できない場合でも、それらを小さなピースに分割する方が良いでしょうか?



7
すべての回答は意見に基づいています。オリジナルの作者は常に、ラビオリのコードは読みやすいと考えています。その後の編集者はそれをラビオリと呼びます。
フランクヒルマン

5
Clean Codeを読むことをお勧めします。このスタイルを推奨し、多くの例があります。また、「ジャンプアラウンド」問題の解決策もあります。
キャット

1
これはあなたのチームと含まれるコード行に依存すると思います。
うまくいけば

1
STRUTSのフレームワーク全体は、使い捨てのGetter / Setterメソッドに基づいていないのですか?
ジボブズ

回答:


203

いいえ、これは悪いスタイルではありません。実際、非常に良いスタイルです。

プライベート関数は、単に再利用性のために存在する必要はありません。それは確かにそれらを作成する理由の1つですが、もう1つは分解です。

機能が多すぎると考えてください。それは百行の長さであり、推論することは不可能です。

この関数をより小さな部分に分割しても、以前と同じくらい多くの作業を「実行」しますが、より小さな部分になります。説明的な名前を持つ他の関数を呼び出します。メイン関数はほとんど本のように読みます。Aを実行し、次にBを実行し、次にCを実行します。呼び出される関数は1か所でしか呼び出せませんが、現在は小さくなっています。特定の関数は他の関数からサンドボックス化される必要があります。それらは異なるスコープを持ちます。

大きな問題を小さな問題に分解すると、小さな問題(関数)が一度だけ使用/解決されても、いくつかの利点が得られます。

  • 読みやすさ。モノリシック関数を読んで、それが完全に何をするのかを理解できる人はいません。嘘をついたり、理にかなっている一口サイズのチャンクに分割したりできます。

  • 参照の局所性。変数を宣言して使用することは不可能になり、その変数をそのまま使用して、100行後に再び使用することができます。これらの関数のスコープは異なります。

  • テスト。クラスのパブリックメンバーを単体テストするだけで十分ですが、特定のプライベートメンバーもテストすることが望ましい場合があります。テストから恩恵を受ける可能性のある長い関数の重要なセクションがある場合、それを別の関数に抽出せずに独立してテストすることは不可能です。

  • モジュール性。プライベート関数が用意できたので、ここでのみ使用するか再利用できるかに関係なく、別のクラスに抽出できる1つ以上の関数を見つけることができます。前の点まで、この別個のクラスはパブリックインターフェイスを必要とするため、テストも簡単になる可能性があります。

大きなコードを理解しやすくテストしやすい小さな断片に分割するというアイデアは、ボブおじさんの本Clean Codeの重要なポイントです。この回答を書いている時点では、この本は9年前ですが、当時と同じように今日でも関連しています。


7
メソッドの内部を覗いても驚かないように、一貫したレベルの抽象化と良い名前を維持することを忘れないでください。オブジェクト全体がそこに隠れていても驚かないでください。
candied_orange

14
分割方法が良い場合もありますが、物事をインラインに保持することにも利点があります。たとえば、コードブロックの内側または外側で境界チェックを適切に実行できる場合、そのコードブロックをインラインに保持すると、境界チェックが1回、複数回、またはまったく実行されていないかどうかを簡単に確認できます。コードのブロックを独自のサブルーチンにプルすると、そのようなことを確認するのが難しくなります。
-supercat

7
「読みやすさ」の箇条書きには「抽象化」というラベルを付けることができます。それは抽象化についてです。「一口サイズのチャンク」、パブリックメソッドを読んだりステップ実行したりする際に、細かい点についてはあまり気にしない、1つの低レベルのことを行います。それを関数に抽象化することで、それを乗り越えて、物事の大きなスキーム/パブリックメソッドがより高いレベルで行っていることに集中し続けることができます。
マチューギンドン

7
「テスト」の箇条書きは疑わしいIMOです。プライベートメンバーのテストを希望する場合は、パブリックメンバーとして、おそらく独自のクラスに属します。確かに、クラス/インターフェースを抽出する最初のステップは、メソッドを抽出することです。
マチューギンドン

6
実際に機能を気にせずに関数を行番号、コードブロック、および変数スコープだけで分割するのはひどい考えです。それよりも優れた代替案は、1つの関数にそのままにしておくことです。機能に応じて機能を分割する必要があります。DaveGauerの答えをご覧ください。あなたはあなたの答えでこれをいくらか示唆しています(名前と問題の言及で)が、私は助けることができませんが、質問との関係でその重要性と関連性を考えると、この側面にはもっと多くの焦点が必要であると感じます。
ダケリング

36

おそらく素晴らしいアイデアです!

コードベースの平均関数の長さを短くするために、アクションの長い線形シーケンスを純粋に個別の関数に分割することに問題があります。

function step1(){
  // ...
  step2(zarb, foo, biz);
}

function step2(zarb, foo, biz){
  // ...
  step3(zarb, foo, biz, gleep);
}

function step3(zarb, foo, biz, gleep){
  // ...
}

今、あなたは実際にソースの行を追加しましたし、かなりの合計可読性を低下させました。特に、状態を追跡するために各関数間で多くのパラメーターを渡す場合は特にそうです。うわぁ!

ただし、1回以上の行を単一の明確な目的に役立つ純粋な関数に抽出できた場合(一度だけ呼び出された場合でも)、読みやすさが向上します。

function foo(){
  f = getFrambulation();
  g = deglorbFramb(f);
  r = reglorbulate(g);
}

これは現実の状況では簡単ではないでしょうが、十分に長く考えれば、純粋な機能の一部がしばしばteaされる可能性があります。

優れた動詞名を持つ関数があり、親関数がそれらを呼び出すと、すべてが実質的に散文の段落のように読み取れるとき、あなたは正しい軌道に乗っていることがわかります。

その後、数週間後に戻って機能を追加し、それらの機能のいずれかを実際に再利用できることに気づいたら、とてつもない喜びです!なんと素晴らしい輝かしい喜びでしょう!


2
私は長年、この議論を聞いてきましたが、現実の世界では決して発揮されません。具体的には、1箇所のみから呼び出される1つまたは2つのライン関数について説明しています。原作者は常に「読みやすい」と考えていますが、その後の編集者はしばしばラビオリコードと呼びます。これは、一般的に言えば、通話の深さに依存します。
フランクヒルマン

34
@FrankHileman公平を期すために、私は開発者が彼または彼女の前任者のコードをほめることを見たことがありません。
T.サー

4
前の開発者である@TSarは、すべての問題の原因です!
フランクヒルマン

18
@TSar私が前の開発者だったとき、私は前の開発者をほめたこともありませんでした。
IllusiveBrian

3
分解の良いところは、次に作曲できることですfoo = compose(reglorbulate, deglorbFramb, getFrambulation);
;;

19

答えは、状況に大きく依存するということです。@Snowmanは、大規模なパブリック関数を分割することのプラス面をカバーしていますが、当然のことながら、マイナス面の影響もあることに注意してください。

  • 副作用のあるプライベート関数がたくさんあると、コードが読みにくくなり、非常に壊れやすくなります。これは、これらのプライベート関数が相互の副作用に依存している場合に特に当てはまります。密結合機能を使用しないでください。

  • 抽象化は漏れやすい。操作の実行方法やデータの保存方法のふりをすることは重要ではありませんが、操作を実行する場合もあり、それらを識別することが重要です。

  • セマンティクスとコンテキストが重要です。関数がその名前で何をするかを明確に把握できなかった場合、再び読みやすくし、公開関数の脆弱性を高めることができます。これは、多数の入力および出力パラメーターを持つプライベート関数の作成を開始する場合に特に当てはまります。そして、パブリック関数からは、それが呼び出すプライベート関数が表示されますが、プライベート関数からは、パブリック関数がそれを呼び出すものが表示されません。これにより、パブリック関数を壊すプライベート関数の「バグ修正」につながる可能性があります。

  • 大きく分解されたコードは、他の人よりも作成者にとって常に明確です。これは、他の人にそれがまだ明確でbar()ないことを意味するものではありませんがfoo()、執筆時に事前に呼び出さなければならない完全な意味があると言うのは簡単です。

  • 危険な再利用。パブリック関数は、各プライベート関数への入力が制限されている可能性があります。これらの入力仮定が適切にキャプチャされない場合(すべての仮定を文書化するのは難しい)、誰かがプライベート関数の1つを不適切に再利用し、コードベースにバグを導入する可能性があります。

絶対的な長さではなく、内部の結合と結合に基づいて関数を分解します。


6
読みやすさを無視して、バグ報告の側面を見てみましょう:ユーザーがバグが発生したことを報告した場合MainMethod(取得するのが簡単で、通常はシンボルが含まれ、シンボル参照ファイルが必要です)、2k行(行番号は含まれません)バグレポート)そしてそれはそれらの行の1kで発生する可能性のあるNREです。しかし、彼らがそれが起こったSomeSubMethodAと言って、それが8行であり、例外がNREであったなら、私はおそらくそれを十分に簡単に見つけて修正するでしょう。
410_ゴーン

2

私見は、単に複雑さを解消するための手段としてコードのブロックを引き出すことの価値であり、多くの場合、以下の間の複雑さの違いに関連しています。

  1. コーナーケースの処理方法を含む、コードの機能に関する完全で正確な人間言語の説明、および

  2. コード自体。

コードが説明よりもはるかに複雑な場合、インラインコードを関数呼び出しに置き換えると、周囲のコードが理解しやすくなります。一方、一部の概念は、人間の言語よりもコンピューター言語で読みやすく表現できます。w=x+y+z;たとえば、の場合よりも読みやすいと思いw=addThreeNumbersAssumingSumOfFirstTwoDoesntOverflow(x,y,z);ます。

大きな関数が分割されると、サブ関数の複雑さとその説明の間に差がますます少なくなり、さらに細分化する利点が減少します。記述がコードよりも複雑になるまで物事が分かれる場合、それ以上のスプリットはコードを悪化させます。


2
あなたの関数名は誤解を招くものです。w = x + y + zには、あらゆる種類のオーバーフロー制御を示すものは何もありませんが、メソッド名自体はそうではないように見えます。関数のより良い名前は、たとえば「addAll(x、y、z)」、または単に「Sum(x、y、z)」です。
T.サー

6
プライベートメソッドについて話しているので、この質問はおそらくCを参照していません。とにかく、それは私のポイントではありません。メソッドシグネチャの仮定に取り組む必要なく、同じ関数「sum」を呼び出すことができます。ロジックでは、たとえば、再帰的なメソッドは「DoThisAssumingStackDoesntOverflow」と呼ばれるべきです。「FindSubstringAssumingStartingPointIsInsideBounds」についても同様です。
T.サー

1
言い換えれば、基本的な仮定は、それらが何であれ(2つの数値がオーバーフローしない、スタックがオーバーフローしない、インデックスが正しく渡された)メソッド名に存在するべきではありません。もちろん、これらのことは必要に応じて文書化する必要がありますが、メソッドの署名には含まれません。
T.サー

1
@TSar:私は名前に少し顔を出していましたが、キーポイントは(より良い例だから平均化に切り替える)コンテキストで「(x + y + z + 1)/ 3」を見るプログラマーはの定義または呼び出しサイトのいずれかを見ている人よりも、それが呼び出しコードを与えられた平均を計算する適切な方法であるかどうかを知るために配置されたほうがはるかに良いint average3(int n1, int n2, int n3)。「平均」機能を分割すると、要件に適しているかどうかを確認したい人は、2つの場所を調べる必要があります。
-supercat

1
たとえば、C#を使用する場合、単一の「Average」メソッドのみがあり、任意の数のパラメーターを受け入れることができます。実際、c#では、あらゆる数字のコレクションに対して機能します。そして、コードに粗雑な数学を加えるよりも、ずっと簡単に理解できると確信しています。
T.サー

2

それはバランスのとれた挑戦です。

細かいほど良い

privateメソッドは、含まれるコードの名前と、時には意味のある署名を効果的に提供します(パラメーターの半分が、明確で文書化されていない相互依存性を持つ完全に特別なトランプデータでない限り)。

コード構成体に名前を付けることは、名前が呼び出し元にとって意味のあるコントラクトを示唆し、プライベートメソッドのコントラクトが名前が示唆するものに正確に対応する限り、一般に適切です。

コードの小さな部分について意味のある契約を考えるように強制することにより、初期開発者はいくつかのバグ発見し、開発テストの前であっても簡単にバグを回避できます。これは、開発者が簡潔な命名(単純に聞こえるが正確)に努めており、簡潔な命名が可能になるようにprivateメソッドの境界を調整する場合にのみ機能します。

追加の命名はコードを自己文書化するのに役立つため、その後のメンテナンスも役立ちます

  • 小さなコードの契約
  • より高いレベルのメソッドは、短い一連の呼び出しに変わることがあります。それぞれの呼び出しは、一般的で最も外側のエラー処理の薄い層を伴って、重要かつ明確に名前が付けられています。このような方法は、モジュールの全体的な構造の専門家にすぐにならなければならない人にとって価値のあるトレーニングリソースになります。

元気になるまで

コードの小さなチャンクに名前を付けすぎて、プライベートメソッドが多すぎて小さすぎてしまう可能性はありますか?承知しました。次の1つ以上の症状は、メソッドが小さすぎて有用ではないことを示しています。

  • 同じ基本的なロジックの代替シグネチャではなく、単一の固定コールスタックを実際に表すオーバーロードが多すぎます。
  • 同じ概念を繰り返し参照するために使用される同義語(「面白い名前付け」)
  • 特にを導入する際に統一されたスキームが存在しない場合は、XxxInternalまたはなどのおかしな名前付けの装飾がたくさんありDoXxxます。
  • 不器用な名前は、実装自体よりもほぼ長い LogDiskSpaceConsumptionUnlessNoUpdateNeeded

1

他の人が言ったことに反して、私は長いパブリックメソッドはプライベートメソッドに分解することによって修正されないデザインの匂いだと主張します。

しかし時々、小さなステップに分割できる大きなパブリックメソッドがあります

この場合、各ステップはそれぞれ1つの責任を持つ独自のファーストクラスの市民でなければならないと主張します。オブジェクト指向のパラダイムでは、各ステップのインターフェイスと実装を作成することをお勧めします。それぞれのステップには、簡単に識別できる単一の責任があり、責任が明確になるように名前を付けることができます。これにより、(以前の)大規模なパブリックメソッドの単体テスト、および各ステップを互いに独立してテストできます。それらもすべて文書化する必要があります。

なぜプライベートメソッドに分解しないのですか?いくつかの理由があります。

  • 密結合とテスト容易性。パブリックメソッドのサイズを小さくすることで、可読性は向上しましたが、すべてのコードは依然として密接に結合されています。(テストフレームワークの高度な機能を使用して)個々のプライベートメソッドを単体テストできますが、パブリックメソッドをプライベートメソッドとは独立して簡単にテストすることはできません。これは、単体テストの原則に反します。
  • クラスのサイズと複雑さ。メソッドの複雑さは減りましたが、クラスの複雑さは増しました。パブリックメソッドは読みやすくなっていますが、クラスの動作を定義する関数が増えているため、クラスは読みにくくなっています。私の好みは小さな単一の責任クラスであるため、長いメソッドはクラスがやりすぎだというサインです。
  • 簡単に再利用できません。多くの場合、コード本体が成熟するにつれて再利用性が役立つことがあります。ステップがプライベートメソッドの場合、何らかの方法で最初に抽出しない限り、他の場所で再利用することはできません。さらに、他の場所でステップが必要な場合は、コピーと貼り付けが推奨されます。
  • このように分割することは任意です。長いパブリックメソッドを分割することは、責任をクラスに分割するのと同じくらい考えたり、設計を考慮したりしないと主張します。各クラスは、適切な名前、ドキュメント、およびテストで正当化される必要がありますが、プライベートメソッドはそれほど考慮されません。
  • 問題を隠します。したがって、パブリックメソッドを小さなプライベートメソッドに分割することにしました。これで問題はありません!より多くのプライベートメソッドを追加することで、より多くのステップを追加し続けることができます!それどころか、これは大きな問題だと思います。クラスに複雑さを追加するパターンを確立し、その後に続くバグ修正と機能の実装が続きます。すぐにプライベートメソッドが大きくなり分割する必要があります。

しかし、メソッドを読んでいる人を強制的に別のプライベートメソッドにジャンプさせると読みやすさが損なわれるのではないかと心配しています

これは、最近同僚の1人と議論したことです。彼は、同じファイル/メソッドにモジュールの全体の動作を持たせることで読みやすさが向上すると主張します。私は、コードがしやすいことに同意し従うとき、すべて一緒に、しかし、コードがしにくく使いやすいですについて推論複雑性が大きくなるにつれて。システムが成長するにつれて、モジュール全体について推論するのが難しくなります。複雑なロジックをそれぞれが単一の責任を持つ複数のクラスに分解すると、各部分について推論するのがはるかに簡単になります。


1
私は反対しなければなりません。抽象化が多すぎるまたは時期尚早であると、コードが不必要に複雑になります。プライベートメソッドは、インターフェイスおよび抽象化が必要になる可能性のあるパスの最初のステップになりますが、ここでのアプローチは、決して訪れる必要のない場所をさまようことを示唆しています。
WillC

1
私はあなたがどこから来たのか理解していますが、コードはOPが尋ねているような臭いが、より良いデザインの準備できている兆候だと思います。早すぎる抽象化は、あなたがその問題に出くわす前にこれらのインターフェースを設計することです。
サミュエル

私はこのアプローチに同意します。実際、TDDに従うとき、それは自然な進行です。他の人は、それがあまりにも早すぎる抽象化であると言いますが、テストに合格するためにプライベートメソッドで実際に実装する必要があるよりも、別のクラスで必要な機能を(インターフェースの後ろで)すばやくモックする方がはるかに簡単であると思います。
Eternal21
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.