web-dev-qa-db-ja.com

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

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

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

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

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

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

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

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

更新:この質問とその下のディスカッションをフォローした場合、お楽しみいただけます この問題に関するJohn Carmackによるこの記事 特に:

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

27
Max Yankov

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

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

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

  • 価値を生み出す
  • 影響を与える

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

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

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

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

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

コードブロックを独自のメソッドに抽出すると、前提条件と事後条件に関するコンテキスト情報が失われます。

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

たとえば、私は以前、コンパイルのいくつかの「段階」を経たコンパイラで作業していました。最初にコードが字句解析され、次に解析され、次に型が解決され、次に継承階層が循環するかどうかがチェックされます。コードのすべての部分は、そのコンテキストに非常に敏感でした。たとえば、「この型はその型に変換可能ですか」と尋ねるのは悲惨です。基本タイプのグラフが非循環であることがまだ知られていない場合!したがって、コードのあらゆる部分がその前提条件を明確に文書化しました。 「ベースタイプアシリック」チェックに合格した型変換可能性をチェックしたメソッドでassertを実行すると、メソッドを呼び出すことができる場所と呼び出せない場所が読者に明らかになりました。 。

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

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

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

新しいクラスは「エクスポーター」または「タブレーション」のような名前を持つ傾向があり、より大きなコンテキストからその特定のタスクを実行するために必要な情報が渡されます。次に、何にでも使用される危険のない、さらに小さなヘルパーコードスニペットを自由に定義できますbutテーブル化またはエクスポート。

13
Kilian Foth

多くの言語では、Haskellのような関数をネストできます。 Java/C#/ C++は、実際にはその点で相対的な外れ値です。残念ながら、それらは非常に人気があり、人々は「それはが悪い考えであるが悪い考えであると思わないようになります。 」

Java/C#/ C++は基本的に、クラスは必要なメソッドの唯一のグループ化であると考えています。メソッドが多すぎてコンテキストを判別できない場合は、2つの一般的な方法があります。それらをコンテキストでソートするか、コンテキストで分割します。

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

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

6
Karl Bielefeldt

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

抽出方法は良いですが、ある程度です。コードを検査するとき、またはコードを書く前に、私はいつも自分にこれらの質問をします。

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

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

4
Tomer Blu

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

これがどれほど大きな問題であるかを理解できなかったのは、ECSを採用して、システムが機能しているループだけが大きく、依存関係が生データに向かって流れ、抽象化ではない、 。

それは、驚いたことに、デバッグ中にあらゆる種類の小さな関数をトレースする必要があった過去のコードベースと比較して、推論と保守がはるかに容易なコードベースを生み出しました。トレースするまで誰がどこを知っているかを導く純粋なインターフェース。コードがこれまでに導くべきではないと思っていた場所につながるイベントのカスケードを生成するためだけです。

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

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

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

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

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

1
user204677

bigの問題ではないと思いますが、問題があることに同意します。通常、ヘルパーを受益者の直後に配置し、「ヘルパー」サフィックスを追加します。これにprivateアクセス指定子を加えると、その役割が明確になります。ヘルパーが呼び出されたときに保持されない不変条件がある場合は、ヘルパーにコメントを追加します。

このソリューションには、役立つ機能のスコープを取得できないという残念な欠点があります。理想的にはあなたの関数は小さいので、うまくいけば、これはあまりにも多くのパラメータをもたらさないでしょう。通常は、パラメーターをまとめるために新しい構造体またはクラスを定義することでこれを解決しますが、そのために必要なボイラープレートの量はヘルパー自体よりも簡単に長くなる可能性があります。関数を持つ構造体。

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

0
Doval