web-dev-qa-db-ja.com

呼び出しチェーンのいくつかのレベルでのみ使用されるパラメーターを渡す(アンチ)パターンの名前はありますか?

私は、いくつかのレガシーコードでグローバル変数を使用する代わりを探していました。しかし、この質問は技術的な選択肢に関するものではなく、主に用語について懸念しています。

明白な解決策は、グローバルを使用する代わりに関数にパラメーターを渡すことです。このレガシーコードベースでは、値が最終的に使用されるポイントと最初にパラメーターを受け取る関数との間の長い呼び出しチェーン内のすべての関数を変更する必要があることを意味します。

_higherlevel(newParam)->level1(newParam)->level2(newParam)->level3(newParam)
_

ここで、newParamは以前の例ではグローバル変数でしたが、代わりに以前にハードコードされた値である可能性があります。ポイントは、newParamの値がhigherlevel()で取得され、level3()までずっと「移動」する必要があることです。

変更せずに値を「渡す」だけの多くの関数にパラメーターを追加する必要があるこのような状況/パターンに名前があるかどうか疑問に思いました。

うまくいけば、適切な用語を使用することで、再設計のためのソリューションに関するより多くのリソースを見つけ、この状況を同僚に説明できるようになるでしょう。

213
ecerulm

データ自体は "tramp data" と呼ばれます。これは「コードのにおい」であり、あるコードが仲介者を介して離れた場所にある別のコードと通信していることを示します。

  • 特に呼び出しチェーンでコードの剛性を高めます。呼び出しチェーン内のメソッドをリファクタリングする方法には、はるかに制約があります。
  • データ/メソッド/アーキテクチャに関する知識を、それについてほとんど気にしない場所に配布します。通過するばかりのデータを宣言する必要があり、その宣言で新しいインポートが必要な場合は、名前空間が汚染されています。

グローバル変数を削除するためのリファクタリングは難しく、トランプデータはそのための1つの方法であり、多くの場合最も安価な方法です。それにはコストがかかります。

205
BobDalgleish

これ自体はアンチパターンではないと思います。問題は、実際にはそれぞれを独立したブラックボックスとして考える必要があるときに、関数をチェーンとして考えていることだと思います([〜#〜]ノート[〜#〜]:再帰的なメソッドは、このアドバイスの顕著な例外です。)

たとえば、2つのカレンダー日付間の日数を計算する必要があるとしましょう。関数を作成します。

int daysBetween(Day a, Day b)

これを行うために、次に新しい関数を作成します。

int daysSinceEpoch(Day day)

それから私の最初の関数は単純になります:

int daysBetween(Day a, Day b)
{
    return daysSinceEpoch(b) - daysSinceEpoch(a);
}

これについてアンチパターンは何もありません。 daysBetweenメソッドのパラメーターは、別のメソッドに渡され、メソッド内で他の方法で参照されることはありませんが、そのメソッドが実行する必要があることを実行するために必要です。

私がお勧めするのは、各関数を見て、いくつかの質問から始めます。

  • この関数には明確で焦点を絞った目標がありますか、それとも「何かを行う」方法ですか?通常、関数の名前が役立ちます。名前で説明されていないものがある場合、それは赤旗です。
  • パラメータが多すぎませんか?メソッドが正当に多くの入力を必要とする場合がありますが、非常に多くのパラメーターがあると、使用または理解するのが面倒になります。

メソッドに単一の目的がバンドルされていない、ごちゃまぜなコードを見ている場合は、まずそれを解明することから始めるべきです。これは退屈な作業です。最も簡単なものから始めて、別の方法に移り、一貫性のあるものになるまで繰り返します。

パラメータが多すぎる場合は、 メソッドからオブジェクトへのリファクタリング を検討してください。

102
JimmyJames

BobDalgleishはすでに注記しています この(アンチ)パターンは「 トランプデータ 」と呼ばれています。

私の経験では、過剰なトランプデータの最も一般的な原因は、実際にはオブジェクトまたはデータ構造にカプセル化されるリンクされた状態変数の束を持っていることです。データを適切に編成するために、オブジェクトの束をネストする必要がある場合さえあります。

簡単な例として、playerNameplayerEyeColorなどのプロパティを持つ、カスタマイズ可能なプレーヤーキャラクターを持つゲームを考えてみましょう。もちろん、プレーヤーはゲームマップ上での物理的な位置や、たとえば現在および最大のヘルスレベルなど、他のさまざまなプロパティも持っています。

そのようなゲームの最初のイテレーションでは、これらのプロパティをすべてグローバル変数にするのは完全に合理的な選択かもしれません。結局のところ、プレイヤーは1人しかいないので、ゲームのほとんどすべてが何らかの形でプレイヤーに関係しています。したがって、グローバル状態には次のような変数が含まれる可能性があります。

_playerName = "Bob"
playerEyeColor = GREEN
playerXPosition = -8
playerYPosition = 136
playerHealth = 100
playerMaxHealth = 100
_

しかし、ある時点で、おそらくゲームにマルチプレイヤーモードを追加したいために、このデザインを変更する必要があるかもしれません。最初の試みとして、これらの変数をすべてローカルにして、それらを必要とする関数に渡してみることができます。ただし、ゲームの特定のアクションに、次のような関数呼び出しチェーンが含まれる場合があります。

_mainGameLoop()
 -> processInputEvent()
     -> doPlayerAction()
         -> movePlayer()
             -> checkCollision()
                 -> interactWithNPC()
                     -> interactWithShopkeeper()
_

...そしてinteractWithShopkeeper()関数は、店主がプレーヤーの名前をアドレスで指定するため、これらの関数をallを介してplayerNameをトランプデータとして突然渡す必要があります。そしてもちろん、店主が青い目をしたプレーヤーはナイーブで、価格が高くなると考えている場合は、関数のチェーン全体にplayerEyeColorを渡す必要があります。

この場合、properソリューションはもちろん、プレーヤーキャラクターの名前、目の色、位置、ヘルス、およびその他のプロパティをカプセル化するプレーヤーオブジェクトを定義することです。そうすれば、プレーヤーに関係するすべての関数に、その単一のオブジェクトを渡すだけで済みます。

また、上記の関数のいくつかは、自然にそのプレーヤーオブジェクトのメソッドにでき、それらは自動的にプレーヤーのプロパティへのアクセスを提供します。ある意味で、これは単なる構文上の砂糖です。オブジェクトのメソッドを呼び出すと、オブジェクトインスタンスが非表示パラメーターとしてメソッドに効果的に渡されますが、適切に使用すると、コードがより明確で自然になります。

もちろん、典型的なゲームはプレイヤーだけではなく、より「グローバルな」状態になります。たとえば、ほぼ確実に、ゲームが行われるある種のマップ、マップ上を移動する非プレイヤーキャラクターのリスト、それにマップ上に配置されたアイテムなどがあるでしょう。これらすべてをトランプオブジェクトとして渡すこともできますが、それでもメソッドの引数が乱雑になります。

代わりに、永続的または一時的な関係を持つ他のオブジェクトへの参照をオブジェクトに格納することで解決策が得られます。したがって、たとえば、プレーヤーオブジェクト(およびおそらくNPCオブジェクトも))は、おそらく現在のレベル/マップへの参照を持つ「ゲームワールド」オブジェクトへの参照を格納する必要があります。そのため、player.moveTo(x, y)のようなメソッドに、マップをパラメーターとして明示的に指定する必要はありません。

同様に、プレーヤーのキャラクターが、たとえばペットの犬を追いかけている場合、犬を記述するすべての状態変数を1つのオブジェクトに自然にグループ化し、プレーヤーオブジェクトに犬への参照を与えます(プレーヤーができるように、たとえば、名前で犬を呼ぶ)とその逆(犬がプレイヤーがどこにいるかを知るため)そしてもちろん、おそらく、プレイヤーと犬のオブジェクトを両方ともより一般的な「actor」オブジェクトのサブクラスにして、同じコードを再利用して、たとえば両方をマップ内で移動したいと思うでしょう。

Ps。例としてゲームを使用しましたが、そのような問題が発生する他の種類のプログラムもあります。ただし、私の経験では、根本的な問題は常に同じになる傾向があります。1つまたは複数の相互リンクされたオブジェクトにまとめる必要がある個別の変数(ローカルまたはグローバル)の束があります。関数に侵入する「トランプデータ」が「グローバル」オプション設定、キャッシュされたデータベースクエリ、または数値シミュレーションの状態ベクトルで構成されているかどうかにかかわらず、解決策は常に自然なコンテキストを特定することですデータが属し、それをオブジェクト(または選択した言語で最も近いもの)にします。

61
Ilmari Karonen

私はこれの具体的な名前を認識していませんが、あなたが説明する問題は、そのようなパラメーターの範囲に対する最良の妥協点を見つける問題にすぎないと言うのは価値があると思います:

  • グローバル変数として、プログラムが特定のサイズに達するとスコープが大きすぎます

  • 純粋にローカルなパラメータとして、スコープが小さすぎる可能性があります。コールチェーンで多くの反復的なパラメータリストにつながる場合

  • したがって、トレードオフとして、そのようなパラメーターを1つ以上のクラスのメンバー変数にすることができます。これをproper class designと呼びます。

34
Doc Brown

あなたが説明しているパターンは正確に 依存性注入 だと思います。これはアンチパターンではなくパターンであるといくつかのコメント者は主張しており、私は同意する傾向があります。

また、@ JimmyJamesの回答にも同意します。彼は、各関数をallの入力を明示的なパラメーターとして受け取るブラックボックスとして扱うことがプログラミングの実践に適していると主張しています。つまり、ピーナッツバターとゼリーのサンドイッチを作る関数を書いている場合、次のように書くことができます。

Sandwich make_sandwich() {
    PeanutButter pb = get_peanut_butter();
    Jelly j = get_jelly();
    return pb + j;
}
extern PhysicalRefrigerator g_refrigerator;
PeanutButter get_peanut_butter() {
    return g_refrigerator.get("peanut butter");
}
Jelly get_jelly() {
    return g_refrigerator.get("jelly");
}

しかし、依存関係注入を適用し、代わりに次のように書くのはより良い実践でしょう。

Sandwich make_sandwich(Refrigerator& r) {
    PeanutButter pb = get_peanut_butter(r);
    Jelly j = get_jelly(r);
    return pb + j;
}
PeanutButter get_peanut_butter(Refrigerator& r) {
    return r.get("peanut butter");
}
Jelly get_jelly(Refrigerator& r) {
    return r.get("jelly");
}

これで、すべての依存関係を関数のシグネチャに明確に文書化する関数ができました。これは読みやすさの点で優れています。結局のところ、それはtrueであり、make_sandwichを実行するにはRefrigeratorにアクセスする必要があります。そのため、古い関数のシグネチャは、冷蔵庫を入力の一部として使用しないことにより、基本的に不誠実でした。

おまけとして、クラス階層を正しく行ったり、スライスを避けたりする場合は、MockRefrigerator!を渡してmake_sandwich関数を単体テストすることもできます。 (ユニットテスト環境がPhysicalRefrigeratorsにアクセスできない可能性があるため、この方法でユニットテストする必要があるかもしれません。)

依存関係注入のすべての使用法がrequireコールスタックのかなり下のレベルにある同様の名前のパラメータを配管しているわけではないので、私はexactly質問に答えていません。 。しかし、この主題についてさらに読むために探しているなら、「依存性注入」は間違いなくあなたに関連するキーワードです。

21
Quuxplusone

これは、ほぼcouplingの教科書定義です。1つのモジュールに依存関係があり、別のモジュールに深く影響し、変更すると波及効果が生じます。他のコメントと回答は正解です。これは、結合が破壊的なものではなく、プログラマにとってより明確で簡単に見えるようになったため、これはグローバルよりも優れていることです。それは修正されるべきではないという意味ではありません。カップリングを削除または削減するためにリファクタリングできるはずですが、それがしばらくそこにある場合は苦痛になる可能性があります。

15
Karl Bielefeldt

この答えは直接はあなたの質問に答えませんが、それを改善する方法について言及せずにそれを通過させるのは間違いだと思います(あなたが言うように、それはアンチパターンである可能性があるため)。あなたと他の読者が「トランプデータ」を回避する方法に関するこの追加の解説から価値を得ることができることを願っています(Bob Dalgleishが私たちのためにそれをわかりやすく名付けたように)。

この問題を回避するためにさらに何かを行うことを提案する回答に同意しますOOこの問題を回避します。しかし、「クラスを渡すだけで、多くの引数を渡すために使用していた場所! "は、プロセスの一部のステップが下位のステップではなく上位のレベルで発生するようにリファクタリングすることです。たとえば、以下にbeforeコードをいくつか示します。

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   FilterAndReportStuff(stuffs, desiredName);
}

public void FilterAndReportStuff(IEnumerable<Stuff> stuffs, string desiredName) {
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   ReportStuff(stuffs.Filter(filter));
}

public void ReportStuff(IEnumerable<Stuff> stuffs) {
   stuffs.Report();
}

これは、ReportStuffで実行する必要があることが多くなるほど、さらに悪化することに注意してください。使用するReporterのインスタンスを渡す必要がある場合があります。そして、ネストされた関数から関数へと渡されなければならないあらゆる種類の依存関係。

私の提案は、これらすべてをより高いレベルに引き上げることです。ステップの知識には、一連のメソッド呼び出しにまたがるのではなく、単一のメソッドでの生活が必要です。もちろん実際のコードではもっと複雑になりますが、これはあなたにアイデアを与えます:

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   var filteredStuffs = stuffs.Filter(filter)
   filteredStuffs.Report();
}

ここでの大きな違いは、長いチェーンを介して依存関係を渡す必要がないことです。 1つのレベルだけでなく、いくつかの深さまで平坦化したとしても、それらのレベルが何らかの「平坦化」を達成し、プロセスがそのレベルでの一連のステップとして認識される場合は、改善されたことになります。

これはまだ手続き型であり、まだオブジェクトに変換されているものはありませんが、何かをクラスに変換することによって実現できるカプセル化の種類を決定するための良いステップです。 beforeシナリオの深くチェーンされたメソッド呼び出しは実際に何が起こっているかの詳細を隠し、コードを非常に理解しにくくする可能性があります。これをやりすぎて、上位レベルのコードにしてはいけないことを知らせたり、多すぎることを実行するメソッドを作成したりして、単一責任の原則に違反する可能性がありますが、一般に、物事を平らにすることは少し助けになることがわかりました明快に、そしてより良いコードに向けて漸進的な変更を加えることにおいて。

このすべてを行っている間、テスト容易性を考慮する必要があることに注意してください。チェーンされたメソッド呼び出しは、実際にユニットテストを実行しますharderテストするスライスのアセンブリに適切な入口点と出口点がないためです。このフラット化により、メソッドはそれほど多くの依存関係を持たなくなるため、テストが容易になり、モックをそれほど必要としません。

私は最近、17の依存関係のようなものをとる(私が記述しなかった)クラスに単体テストを追加しようとしましたが、それらすべてをモックする必要がありました!まだうまくいっていませんが、クラスを3つのクラスに分割し、それぞれが関連する個別の名詞の1つを処理し、依存関係リストを最悪の場合は12、最悪の場合は約8にしました。一番いいもの。

テスト容易性はforceより良いコードを記述します。ユニットテストを作成する必要があります。ユニットテストを作成する前に発生したバグの数に関係なく、コードを別の方法で考えることができ、最初から優れたコードを作成できるからです。

3
ErikE

あなたは文字通りデメテルの法則に違反しているわけではありませんが、あなたの問題はいくつかの点でそれに似ています。あなたの質問のポイントはリソースを見つけることですので、私はデメテルの法則について読んで、あなたの状況にそのアドバイスがどれだけ当てはまるかを見てみることをお勧めします。

2
catfood

常にすべてを渡すオーバーヘッドではなく、特定の変数をグローバルとして使用するのが最適な場合(効率、保守性、実装の容易さの点で)(永続化する必要がある変数が15個程度あるとします)。したがって、(C++のプライベート静的変数として)スコープをより適切にサポートするプログラミング言語を見つけて、(名前空間や改ざんされた)潜在的な混乱を軽減することは理にかなっています。もちろん、これは単なる常識です。

しかし、OPで述べられているアプローチは、関数型プログラミングをしている場合は非常に便利です。

1
kozner

発信者はこれらのすべてのレベルについて知らず、気にしないので、ここにはアンチパターンはありません。

誰かがhigherLevel(params)を呼び出しており、higherLevelがその仕事をすることを期待しています。 lowerLevelがparamsで行うことは、呼び出し元のビジネスではありません。 lowerLevelは問題を可能な限り最善の方法で処理します。この場合、paramsをlevel1(params)に渡します。それは絶対に大丈夫です。

呼び出しチェーンが表示されますが、呼び出しチェーンはありません。最上部にはできる限り最善の方法で機能する機能があります。そして、他の機能があります。各機能はいつでも置き換えることができます。

0
gnasher729