web-dev-qa-db-ja.com

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

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

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

141
Jordak

いいえ、これは悪いスタイルではありません。実際、とても良いスタイルです。

再利用性があるからといって、プライベート関数が存在する必要はありません。それは確かにそれらを作成する1つの正当な理由ですが、別の理由があります。それは分解です。

やり過ぎる関数を考えてみましょう。それは100行の長さであり、推論することは不可能です。

この関数を細かく分割しても、以前と同じように「機能」しますが、細かく分割されます。説明的な名前を持つ他の関数を呼び出します。メイン関数は本のように読みます:do A、do B、次にCなど。特定の関数は必ず他の関数からサンドボックス化されます。それらは異なるスコープを持っています。

大きな問題を小さな問題に分解すると、それらの小さな問題(関数)が一度しか使用または解決されない場合でも、いくつかの利点があります。

  • 読みやすさ。モノリシック関数を読んで、それが何をするのかを完全に理解することはできません。あなたは自分自身に嘘をつくか、または意味のある一口サイズのチャンクに分割することができます。

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

  • テスト。クラスのpublicメンバーを単体テストする必要があるだけですが、特定のプライベートメンバーもテストすることが望ましい場合があります。テストから利益を得る可能性がある長い関数のクリティカルセクションがある場合、別の関数に抽出せずに独立してテストすることは不可能です。

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

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

204
user22815

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

問題はありますアクションの長い線形シーケンスを個別の関数に純粋に分割して、コードベースの平均関数長を短くします。

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);
}

これは実際の状況では簡単ではない可能性がありますが、十分に長い間考えた場合、純粋な機能の一部が取り除かれることがよくあります。

ニースの動詞名を持つ関数があり、親関数がそれらを呼び出して、全体が実際には散文の段落のように読み取れる場合、正しい方向に進んでいることがわかります。

次に、数週間後に戻って機能を追加し、実際にこれらの関数の1つを再利用でき、次にoh、猛烈な喜び!驚くべき輝かしい喜び!

36
DaveGauer

答えは、状況によって大きく異なります。 @Snowmanは、大規模な公開機能を分割することのメリットをカバーしていますが、あなたが正しく懸念しているように、マイナスの影響もある可能性があることを覚えておくことは重要です。

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

  • 抽象化はリーキーです 。操作の実行方法やデータの保存方法のふりをするのは重要ではありませんが、場合によっては、操作を特定することが重要です。

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

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

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

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

19
dlasalle

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

フィナーが優れています

Privateメソッドは、含まれているコードにnameを効果的に提供し、場合によっては意味のある署名を提供します(パラメーターの半分が完全にアドホックでない限り トランプデータ 不明確で、文書化されていない相互依存関係がある場合)。

名前が呼び出し側に意味のあるcontractを示唆し、プライベートメソッドのコントラクトがその名のとおり。

コードの小さな部分について意味のあるコントラクトを自分で考えさせることで、初期の開発者はバグを見つけ、開発テストの前であっても苦痛なくそれらを回避できます。これが機能するのは、開発者が簡潔な命名を目指しており(単純に聞こえるが正確)、プライベートメソッドの境界を適応させて、簡潔な命名が可能になる場合のみです。

追加の名前付けにより、コードが自己文書化されるので、その後のメンテナンスも役立ちます。

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

細かくなり過ぎるまで

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

  • 同じ必須ロジックの代替シグネチャを実際に表すのではなく、単一の固定呼び出しスタックであるオーバーロードが多すぎます。
  • 同義語は、同じ概念を繰り返し参照するためだけに使用されます(「面白いネーミング」)
  • XxxInternalDoXxxなどの、おかしな命名の装飾がたくさんあります。特に、それらを導入するための統一されたスキームがない場合はそうです。
  • 不器用な名前は、LogDiskSpaceConsumptionUnlessNoUpdateNeededのように、実装自体よりもほとんど長くなります
2
Jirka Hanika

私見、純粋に複雑さを解体する手段としてコードのブロックを引き出すことの価値は、しばしば以下の間の複雑さの違いに関連しているでしょう:

  1. コードが行うことの完全で正確な人間の言語による記述コーナーケースの処理方法を含む、および

  2. コード自体。

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

大きな関数が分割されると、サブ関数の複雑さとその説明の間に差異が少なくなり、さらに細分割することの利点は減少します。記述がコードよりも複雑になるまで分割すると、さらに分割するとコードが悪化します。

2
supercat

他の人が言ったこととは反対に、長い公開方法はnotによって修正されたデザインの匂いであると私は主張しますプライベートメソッドに分解します。

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

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

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

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

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

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

1
Samuel