メソッド抽出と基礎となる仮定


27

大きなメソッド(またはプロシージャ、または関数—この質問 OOPに固有のものではありませんが、私は99%の時間でOOP言語で作業しているため、私が最も使いやすい用語です) 、結果に不満を感じることがよくあります。これらの小さなメソッドについては、大きなメソッドの単なるコードブロックである場合よりも推論するのが難しくなります。なぜなら、それらを抽出すると、呼び出し元のコンテキストに由来する多くの基本的な前提が失われるためです。

後で、このコードを見て個々のメソッドを見ると、それらがどこから呼び出されているかすぐにはわかりません。ファイル内のどこからでも呼び出すことができる通常のプライベートメソッドと考えています。たとえば、初期化メソッド(コンストラクターなど)を一連の小さなメソッドに分割することを想像してください:メソッド自体のコンテキストでは、オブジェクトの状態がまだ無効であることを明確に知っていますが、通常のプライベートメソッドでは、おそらくはすでに初期化されており、有効な状態です。

これについて私が見た唯一の解決策は、whereHaskell の句です。これにより、「親」関数でのみ使用される小さな関数を定義できます。基本的には次のようになります。

len x y = sqrt $ (sq x) + (sq y)
    where sq a = a * a

しかし、私が使用する他の言語にはこのようなものはありません。最も近いのは、ローカルスコープでラムダを定義することです。

だから、私の質問は-あなたはこれに遭遇しますか、そしてあなたはこれが問題であることを見ますか?そうした場合、特にJava / C#/ C ++などの「メインストリーム」OOP言語では、通常どのように解決しますか?

重複についての編集:他の人が気づいたように、分割方法を議論する質問とワンライナーである小さな質問が既にあります。私はそれらを読みましたが、呼び出し元のコンテキスト(上記の例では、オブジェクトが初期化されている)から派生する可能性のある基本的な前提の問題については説明していません。それが私の質問のポイントであり、それが私の質問が異なる理由です。

更新:この質問と議論を下で行った場合、特にこの問題に関するJohn Carmackの記事をお楽しみください。

実行されている実際のコードを認識することに加えて、インライン関数には、他の場所から関数を呼び出せないという利点もあります。それはばかげているように聞こえますが、それにはポイントがあります。コードベースが長年の使用で成長するにつれて、ショートカットを使用して、実行する必要があると思われる作業のみを実行する関数を呼び出す機会が多くなります。PartialUpdateA()およびPartialUpdateB()を呼び出すFullUpdate()関数があるかもしれませんが、特定のケースでは、PartialUpdateB()を実行するだけでよいことに気付く(または考える)可能性があり、他を避けることで効率的です作業。これには多くのバグがたくさんあります。ほとんどのバグは、実行状態があなたが思っているとおりではないという結果です。




@gnatあなたがリンクした質問は、関数をまったく抽出するかどうかを議論しますが、私は質問しません。代わりに、私はそれを行うための最適な方法を疑問視しています。
マックスヤンコフ

2
@gnatには、そこからリンクされた他の関連する質問がありますが、これらのいずれも、このコードが呼び出し側のコンテキストでのみ有効な特定の仮定に依存する可能性があるという事実を説明していません。
マックスヤンコフ

1
私の経験では@Doval、本当にそうです。説明のように厄介なヘルパーメソッドがぶらぶらしている場合は、新しい凝集クラスを抽出することでこれを処理できます
-gnat

回答:


29

たとえば、一連の小さなメソッドに分割された初期化メソッドを想像してください:メソッド自体のコンテキストでは、オブジェクトの状態がまだ無効であることを明確に知っていますが、通常のプライベートメソッドでは、おそらくオブジェクトがすでに初期化されており、有効な状態。私がこれについて見た唯一の解決策は...

あなたの懸念は根拠があります。別の解決策があります。

下がってください。メソッドの目的は基本的に何ですか?メソッドは、次の2つのいずれかのみを実行します。

  • 値を生成する
  • 効果を引き起こす

または、残念ながら、両方。私は両方を行う方法を避けようとしますが、多くの方法を行います。生成された効果または生成された値がメソッドの「結果」であるとしましょう。

メソッドは「コンテキスト」で呼び出されることに注意してください。その文脈は何ですか?

  • 引数の値
  • メソッド外のプログラムの状態

本質的にあなたが指摘しているのは、メソッドの結果の正確さは、呼び出されるコンテキストに依存するということです

私たちは、呼び出しメソッド本体が正しい結果を生成するための方法のために開始する前に必要な条件前提条件、および我々は呼び出すメソッド本体が戻った後に生成される条件事後条件を

つまり、本質的にあなたが指摘しているのは、コードブロックを独自のメソッドに抽出すると、前提条件と事後条件に関するコンテキスト情報が失われるということです

この問題の解決策は、プログラムで前提条件と事後条件を明示的にすることです。たとえば、C#では、Debug.Assert前提条件と事後条件を表現するために、またはコードコントラクトを使用できます。

例:私は、コンパイルのいくつかの「段階」を経て動くコンパイラで働いていました。最初にコードが字句解析され、解析され、次に型が解決され、次に継承階層のサイクルがチェックされます。コードのすべてのビットは、そのコンテキストに非常に敏感でした。たとえば、「この型はその型に変換可能ですか?」と尋ねるのは悲惨です。基本型のグラフが非周期的であることがまだ知られていない場合!したがって、コードの各ビットは、その前提条件を明確に文書化しています。私たちは考えassertメソッドが呼び出される可能性がどこ我々はすでに、「基本型非環式」のチェックを通過した、そしてそれは、その後、読者に明らかになったことをタイプ兌換をチェック方法、どこでそれを呼び出すことができませんでした。

もちろん、優れたメソッド設計により、特定した問題を軽減する方法はたくさんあります。

  • 両方の効果ではなく、効果や価値に役立つメソッドを作成します
  • できるだけ「純粋」なメソッドを作成します。「純粋な」メソッドは、引数のみに依存する値を生成し、効果を生成しません。これらは、必要な「コンテキスト」が非常にローカライズされているため、推論する最も簡単な方法です。
  • プログラム状態で発生する突然変異の量を最小限に抑える。突然変異は、コードが推論するのが難しくなるポイントです

前提条件/事後条件の観点から問題を説明する答えであることに対して+1。
QuestionC

5
事前条件と事後条件のチェックを型システムに委任することは、多くの場合可能です(そして、良いアイデアです!)。stringを取得してデータベースに保存する関数がある場合、それを削除するのを忘れるとSQLインジェクションの危険にさらされます。一方、関数がSanitisedStringを取得し、を取得する唯一の方法SantisiedStringがを呼び出すことSanitiseである場合、構築によりSQLインジェクションのバグを排除しました。コンパイラに不正なコードを拒否させる方法を探していることがますます増えています。
ベンジャミンホジソン

+1重要なことの1つは、大きなメソッドを小さなチャンクに分割するためのコストがあることです。通常、前提条件と後条件が元の状態よりも緩和されていない限り、役に立ちません。他の方法で既に行っていたであろうチェックを再実行することにより、コストを支払います。完全に「無料」のリファクタリングプロセスではありません。
Mehrdad

「そのコンテキストは何ですか?」明確にするために、私は主にこのメソッドが呼び出されるオブジェクトのプライベート状態を意味しました。2番目のカテゴリに含まれていると思います。
マックスヤンコフ

これは素晴らしい、そして示唆に富む答えです、ありがとう。(もちろん、他の答えが悪いと言っているわけではありません)。私はここでの議論が本当に好きなので(そして回答が回答済みとしてマークされると終了する傾向がある)、質問をまだ回答済みとしてマークせず、それを処理して考える時間を必要とします。
マックスヤンコフ

13

私はよくこれを見て、それが問題であることに同意します。通常、メソッドオブジェクトを作成することで解決しますメソッドオブジェクトは、元の大きすぎるメソッドのローカル変数をメンバーとする新しい特殊なクラスです。

新しいクラスは「Exporter」や「Tabulation」などの名前を持つ傾向があり、より大きなコンテキストからその特定のタスクを実行するために必要な情報がすべて渡されます。次に表作成またはエクスポート以外に使用される危険のない、より小さなヘルパーコードスニペットを自由に定義できます。


私はそれについて考えるほど、このアイデアが本当に好きです。パブリッククラスまたは内部クラス内のプライベートクラスにすることができます。非常にローカルにしか気にしないクラスで名前空間を乱雑にすることはありません。これは、これらが「コンストラクタヘルパー」または「解析ヘルパー」などであることを示す方法です。
マイクはモニカをサポートします

最近、私はちょうどアーキテクチャの観点からこれに理想的な状況にありました。レンダラークラスとパブリックレンダリングメソッドを使用してソフトウェアレンダラーを作成しました。このメソッドには、他のメソッドを呼び出すために使用されるコンテキストがたくさんありました。このために別のRenderContextクラスを作成することを考えましたが、フレームごとにこのプロジェクトを割り当てたり割り当て解除したりするのは非常に無駄に思えました。github.com/golergka/tinyrenderer/blob/master/src/renderer.h
マックスヤンコフ

6

多くの言語では、Haskellのような関数をネストできます。Java / C#/ C ++は、実際にはその点で相対的な外れ値です。残念ながら、それらは非常に人気があり、人々は「それ悪い考えでなければなりません。そうでなければ、私のお気に入りの「主流」言語はそれを許すでしょう。」

Java / C#/ C ++は基本的に、クラスが必要なメソッドの唯一のグループであると考えています。コンテキストを判断できないほど多くのメソッドがある場合、2つの一般的なアプローチがあります。コンテキストごとにソートする方法、またはコンテキストごとに分割する方法です。

コンテキストによる並べ替えは、クリーンコードで行われた推奨事項の1つであり、著者は「TO段落」のパターンを説明しています。これは基本的に、ヘルパー関数を呼び出す関数の直後にヘルパー関数を配置することです。したがって、新聞記事の段落のように読むことができ、読むほど詳細が得られます。彼のビデオでは、彼はそれらをインデントしているとさえ思います。

もう1つの方法は、クラスを分割することです。これは、オブジェクトのメソッドを呼び出す前にオブジェクトをインスタンス化する必要があるという面倒な必要性と、いくつかの小さなクラスのどれが各データを所有するかを決定する際の固有の問題のため、それほど遠くまではとれません。ただし、実際に1つのコンテキストにのみ適合するいくつかのメソッドを既に特定している場合は、おそらく独自のクラスに入れることを検討するのに適した候補です。たとえば、ビルダーのような作成パターンで複雑な初期化を行うことができます。


関数の入れ子...ラムダ関数がC#(およびJava 8)で実現することではありませんか?
アルトゥーロトーレスサンチェス

これらのpythonの例のように、名前で定義されたクロージャーのように考えていました。ラムダはそのようなことをする最も明確な方法ではありません。フィルタ述語のような短い式に向いています。
カールビーレフェルト

これらのPythonの例は、C#では確かに可能です。たとえば、階乗です。より冗長かもしれませんが、100%可能です。
アルトゥーロトレスサンチェス

2
誰もそれが不可能だとは言いませんでした。OPは彼の質問でラムダを使用することさえ言及しました。読みやすくするためにメソッドを抽出する場合、それがより読みやすいといいのです。
カールビーレフェルト

あなたの最初の段落は、特にあなたの引用で、それが不可能であることを暗示しているようです:「それは悪い考えでなければなりません。
アルトゥーロトーレスサンチェス

4

ほとんどの場合、答えはコンテキストだと思います。コードを作成する開発者は、コードが将来変更されると想定する必要があります。クラスは、別のクラスと統合されたり、内部アルゴリズムを置き換えたり、抽象化を作成するためにいくつかのクラスに分割されたりする場合があります。これらは初心者開発者が通常考慮しないものであり、面倒な回避策や完全なオーバーホールが後で必要になります。

メソッドを抽出することは良いことですが、ある程度までです。私は常に、コードを検査するとき、またはコードを書く前にこれらの質問を自問自答しようとします。

  • このコードはこのクラス/関数でのみ使用されますか?将来も同じままですか?
  • 具体的な実装の一部を切り替える必要がある場合、簡単に実行できますか?
  • 私のチームの他の開発者は、この機能で何が行われているのか理解できますか?
  • このクラスのどこかで同じコードが使用されていますか?ほとんどすべての場合、重複を避ける必要があります。

いずれにせよ、常に単一の責任を考えてください。クラスには1つの責任があり、その機能は1つの単一の一定のサービスを提供する必要があります。また、複数のアクションを実行する場合、それらのアクションには独自の機能があるため、後で区別したり変更したりするのは簡単です。


1

これらの小さなメソッドについては、大きなメソッドの単なるコードブロックである場合よりも推論するのが難しくなります。なぜなら、それらを抽出すると、呼び出し元のコンテキストに由来する多くの基本的な前提が失われるためです。

ECSを採用するまで、これがどれほど大きな問題であるかを理解していませんでした。ECSを使用すると、抽象化ではなく、より大きなループのシステム機能(機能を持つシステムのみ)と依存関係が生データに流れます。

驚いたことに、デバッグ中にあらゆる種類の小さな機能、多くの場合、トレースするまで誰がどこにいるかを知るための純粋なインターフェースであり、コードがこれまでリードするとは思わなかった場所につながるイベントのカスケードを発生させるだけです。

John Carmackとは異なり、これらのコードベースでの最大の問題はパフォーマンスではありませんでした。AAAゲームエンジンの非常に厳しいレイテンシ要求と、パフォーマンスの問題のほとんどがスループットに関連していなかったからです。もちろん、その構造が邪魔されることなく、ティーンエイジャーやティーンエイジャーの関数やクラスのより狭い範囲で作業しているときに、ホットスポットを最適化することがますます難しくなる可能性があります効果的に取り組むことさえできるようになる前に、より大きな何かに)。

しかし、私にとって最大の問題は、すべてのテストに合格したにもかかわらず、システムの全体的な正確性について自信を持って推論できないことでした。そのタイプのシステムは、これらすべての小さな詳細と、どこでも起こっている小さな機能とオブジェクトとの間の無限の相互作用を考慮せずに、あなたにそれを推論させなかったので、私の脳に取り入れて理解することが多すぎました。「what ifs?」が多すぎたり、適切なタイミングで呼び出す必要のあるものが多すぎたり、間違ったタイミングで呼び出された場合に何が起こるかについての質問が多すぎます1つのイベントが別のイベントをトリガーして別のイベントをトリガーして、あらゆる種類の予測不可能な場所に誘導します)など。

今では、私が大きなお尻の80行関数が好きです。ただし、それらがまだ特異で明確な責任を果たし、8レベルのネストされたブロックを持たない限りです。これらの大きな機能の小さくて細分化されたバージョンは、他の誰からも呼び出すことができないプライベートな実装の詳細でしかなかったとしても、システムにテストして理解するものが少ないという感じにつながります...まだ、どういうわけか、システム全体で行われている相互作用が少ないように感じる傾向があります。複雑なロジック(たとえば、2〜3行のコード)でない限り、機能が少ない場合は、非常に控えめなコードの複製も好きです。私は、その機能をソースファイルの他の場所で呼び出すことを不可能にするインライン化についてのカーマックの推論が好きです。そこ'

オプションが1つの肉付きの良い関数と、依存関係の複雑なグラフで相互に呼び出す12の非常に単純な関数の間にある場合、単純さは常に全体像レベルで複雑さを軽減しません。一日の終わりには、関数を超えて何が起こるかについて多くの場合推論する必要があり、これらの関数が最終的に何を実行するかについて推論する必要があります。最小のパズルのピース。

もちろん、十分にテストされた非常に汎用的なライブラリタイプコードは、このルールを免除できます。そのような汎用コードは、多くの場合、機能し、単独でうまく機能するためです。また、アプリケーションのドメインに少し近いコード(数百万行ではなく、数千行のコード)と比較すると、ちっぽけな傾向があり、非常に広く適用できるため、毎日の語彙の一部になり始めています。しかし、システム固有の不変条件が単一の関数やクラスをはるかに超えているという、アプリケーション固有の何かがあると、何らかの理由でより機能的な関数を使用するのに役立つことがわかります。大きな絵で何が起こっているのかを理解しようとすると、大きなパズルのピースを使って作業する方がずっと簡単です。


0

大きな問題だとは思いませんが、面倒なことに同意します。通常、受益者の直後にヘルパーを配置し、「ヘルパー」接尾辞を追加します。それにprivateアクセス指定子を加えると、その役割が明確になります。ヘルパーが呼び出されたときに保持されない不変式がある場合、ヘルパーにコメントを追加します。

このソリューションには、それが役立つ機能の範囲をキャプチャしないという不幸な欠点があります。理想的には、関数は小さいので、これによりパラメーターが多くなりすぎないことを願っています。通常、新しい構造体またはクラスを定義してパラメーターをまとめることでこれを解決しますが、そのために必要なボイラープレートの量はヘルパー自体よりも簡単に長くなる可能性があり、明確な関連付けのない最初の場所に戻ります関数を含む構造体。

あなたはすでに他の解決策について言及しました-メイン関数内でヘルパーを定義します。一部の言語ではあまり一般的ではないイディオムかもしれませんが、混乱を招くとは思いません(ピアが一般的にラムダに混乱しない限り)。ただし、関数または関数のようなオブジェクトを簡単に定義できる場合にのみ機能します。たとえば、Java 7ではこれを試しません。匿名クラスでは、最小の「関数」でも2レベルのネストを導入する必要があるためです。これは、取得できる限りletor where句に近いものです。定義の前にローカル変数を参照でき、ヘルパーはそのスコープ外では使用できません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.