web-dev-qa-db-ja.com

パラメーターが多すぎる(6+)メソッドをリファクタリングする最良の方法は何ですか?

時折、不快な数のパラメーターを持つメソッドに遭遇します。多くの場合、それらはコンストラクターのようです。もっと良い方法があるはずのように思えますが、それが何なのかわかりません。

return new Shniz(foo, bar, baz, quux, fred, wilma, barney, dino, donkey)

パラメータのリストを表すために構造体を使用することを考えましたが、それは問題をある場所から別の場所に移し、その過程で別のタイプを作成するようです。

ShnizArgs args = new ShnizArgs(foo, bar, baz, quux, fred, wilma, barney, dino, donkey)
return new Shniz(args);

したがって、それは改善のようには見えません。それでは、最善のアプローチは何ですか?

89
recursive

最善の方法は、引数をグループ化する方法を見つけることです。これは、引数の複数の「グループ化」で終わる場合にのみ、実際に機能します。

たとえば、長方形の仕様を渡す場合、x、y、幅、および高さを渡すことができます。または、x、y、幅、および高さを含む長方形オブジェクトを渡すこともできます。

リファクタリングするときにこのようなものを探して、ある程度クリーンアップします。引数を実際に組み合わせることができない場合は、単一責任原則に違反しているかどうかを調べ始めます。

93

私はあなたがC#を意味すると仮定するつもりです。これらのことのいくつかは、他の言語にも適用されます。

いくつかのオプションがあります:

コンストラクターからプロパティセッターに切り替えます。これにより、どの値がどのパラメーターに対応するかが読者に明らかであるため、コードが読みやすくなります。オブジェクト初期化子の構文により、見た目が良くなります。また、自動生成されたプロパティを使用して、コンストラクターの作成をスキップできるため、実装も簡単です。

_class C
{
    public string S { get; set; }
    public int I { get; set; }
}

new C { S = "hi", I = 3 };
_

ただし、不変性が失われ、コンパイル時にオブジェクトを使用する前に必要な値が設定されていることを確認できなくなります。

Builderパターン

stringStringBuilderの関係について考えてください。独自のクラスでこれを取得できます。ネストされたクラスとして実装するのが好きなので、クラスCには関連するクラス_C.Builder_があります。ビルダーの流なインターフェイスも気に入っています。完了したら、次のような構文を取得できます。

_C c = new C.Builder()
    .SetX(4)    // SetX is the fluent equivalent to a property setter
    .SetY("hello")
    .ToC();     // ToC is the builder pattern analog to ToString()

// Modify without breaking immutability
c = c.ToBuilder().SetX(2).ToC();

// Still useful to have a traditional ctor:
c = new C(1, "...");

// And object initializer syntax is still available:
c = new C.Builder { X = 4, Y = "boing" }.ToC();
_

これを行うためのビルダーコードを生成できるPowerShellスクリプトがあり、入力は次のようになります。

_class C {
    field I X
    field string Y
}
_

したがって、コンパイル時に生成できます。 partialクラスを使用すると、生成されたコードを変更せずにメインクラスとビルダーの両方を拡張できます。

「パラメーターオブジェクトの導入」リファクタリングリファクタリングカタログ を参照してください。これは、渡すパラメーターの一部を取得して新しい型に入れ、その型のインスタンスを代わりに渡すという考え方です。考えずにこれを行うと、開始した場所に戻ります。

_new C(a, b, c, d);
_

になる

_new C(new D(a, b, c, d));
_

ただし、このアプローチは、コードにプラスの影響を与える可能性が最も高くなります。したがって、次の手順に従って続行します。

  1. 一緒に意味のあるパラメーターのサブセットを探します。関数のすべてのパラメーターを意図せずにグループ化しても、あまり意味がありません。目標は、意味のあるグループを作成することです。 新しい型の名前が明らかなとき、あなたはそれが正しいことを知っているでしょう。

  2. これらの値が一緒に使用される他の場所を探して、そこで新しいタイプも使用します。おそらく、あちこちで既に使用している一連の値に適した新しい型を見つけた場合、その新しい型もそれらのすべての場所で意味があります。

  3. 既存のコードにはあるが、新しいタイプに属する機能を探します。

たとえば、次のようなコードが表示される場合があります。

_bool SpeedIsAcceptable(int minSpeed, int maxSpeed, int currentSpeed)
{
    return currentSpeed >= minSpeed & currentSpeed < maxSpeed;
}
_

minSpeedおよびmaxSpeedパラメーターを使用して、新しいタイプに入れることができます。

_class SpeedRange
{
   public int Min;
   public int Max;
}

bool SpeedIsAcceptable(SpeedRange sr, int currentSpeed)
{
    return currentSpeed >= sr.Min & currentSpeed < sr.Max;
}
_

これは優れていますが、新しいタイプを実際に活用するには、比較を新しいタイプに移動します。

_class SpeedRange
{
   public int Min;
   public int Max;

   bool Contains(int speed)
   {
       return speed >= min & speed < Max;
   }
}

bool SpeedIsAcceptable(SpeedRange sr, int currentSpeed)
{
    return sr.Contains(currentSpeed);
}
_

そして、now私たちはどこかに到達しています:SpeedIsAcceptable()の実装は、あなたが何を意味するかを伝え、有用で再利用可能なクラスを持っています。 (次の明白なステップは、SpeedRangeを_Range<Speed>_にすることです。)

ご覧のとおり、Introduce Parameter Objectは良い出発点でしたが、実際の価値は、モデルから欠落している有用な型を発見する助けとなったことです。

99
Jay Bazuzi

コンストラクターの場合、特にオーバーロードされたバリアントが複数ある場合は、Builderパターンを確認する必要があります。

Foo foo = new Foo()
          .configBar(anything)
          .configBaz(something, somethingElse)
          // and so on

通常のメソッドの場合は、渡される値間の関係を考慮し、おそらくTransfer Objectを作成する必要があります。

20
kdgregory

これは、ファウラーとベックの本から引用されています:「リファクタリング」

長いパラメーターリスト

プログラミングの初期の頃、私たちは、ルーチンに必要なすべてをパラメーターとして渡すように教えられました。代替手段はグローバルデータであり、グローバルデータは悪で、通常は苦痛であるため、これは理解できました。オブジェクトがこの状況を変えるのは、必要なものがなければ、いつでも別のオブジェクトに依頼してもらうことができるからです。したがって、オブジェクトでは、メソッドに必要なすべてを渡すわけではありません。代わりに、メソッドが必要なものすべてに到達できるように十分に渡します。メソッドに必要なものの多くは、メソッドのHostクラスで利用できます。オブジェクト指向プログラムでは、パラメータリストは従来のプログラムよりもはるかに小さくなる傾向があります。これは、長いパラメータリストを理解するのが難しく、一貫性がなく使用が困難になるため、さらにデータが必要になったときにリストを永久に変更するためです。ほとんどの変更はオブジェクトを渡すことで削除されます。これは、新しいデータを取得するために必要なリクエストは数回で済むためです。既に知っているオブジェクトのリクエストを作成して、1つのパラメーターでデータを取得できる場合は、パラメーターをメソッドで置換を使用します。このオブジェクトはフィールドの場合もあれば、別のパラメーターの場合もあります。 [オブジェクト全体を保持]を使用して、オブジェクトから収集した大量のデータを取得し、オブジェクト自体に置き換えます。論理オブジェクトのないデータ項目が複数ある場合は、パラメーターオブジェクトの導入を使用します。これらの変更を行うには、1つの重要な例外があります。これは、呼び出されたオブジェクトからより大きなオブジェクトへの依存関係を明示的に作成したくない場合です。これらの場合、データを解凍してパラメーターとして送信するのは合理的ですが、関連する痛みに注意してください。パラメーターリストが長すぎるか、頻繁に変更される場合は、依存関係構造を再考する必要があります。

10
Youssef

これに対する古典的な答えは、クラスを使用して一部またはすべてのパラメーターをカプセル化することです。理論的には素晴らしいように聞こえますが、私はドメイン内で意味を持つ概念のクラスを作成する人なので、このアドバイスを適用するのは必ずしも簡単ではありません。

例えば。の代わりに:

driver.connect(Host, user, pass)

使用できます

config = new Configuration()
config.setHost(Host)
config.setUser(user)
config.setPass(pass)
driver.connect(config)

YMMV

9
Wouter Lievens

長いパラメーターリストが表示されるとき、最初の質問は、この関数またはオブジェクトが多すぎるかどうかです。考慮してください:

EverythingInTheWorld earth=new EverythingInTheWorld(firstCustomerId,
  lastCustomerId,
  orderNumber, productCode, lastFileUpdateDate,
  employeeOfTheMonthWinnerForLastMarch,
  yearMyHometownWasIncorporated, greatGrandmothersBloodType,
  planetName, planetSize, percentWater, ... etc ...);

もちろん、この例は意図的にばかげていますが、同じ呼び出しプログラムが両方を必要とするか、またはプログラマは偶然両方を同時に思いついた。簡単な解決策は、クラスを複数の部分に分割し、それぞれが独自の処理を行うことです。

ほんの少し複雑なのは、顧客の注文や顧客に関する一般的な情報など、クラスが複数の論理的な事柄を実際に処理する必要がある場合です。これらの場合、顧客用のクラスと注文用のクラスを作成し、必要に応じて互いに話し合うようにします。代わりに:

 Order order=new Order(customerName, customerAddress, customerCity,
   customerState, customerZip,
   orderNumber, orderType, orderDate, deliveryDate);

私たちは持つことができました:

Customer customer=new Customer(customerName, customerAddress,
  customerCity, customerState, customerZip);
Order order=new Order(customer, orderNumber, orderType, orderDate, deliveryDate);

もちろん、1つまたは2つまたは3つのパラメーターのみを受け取る関数を好みますが、現実的には、この関数が大量に必要であり、それ自体の数が実際に複雑さを引き起こさないことを受け入れる必要があります。例えば:

Employee employee=new Employee(employeeId, firstName, lastName,
  socialSecurityNumber,
  address, city, state, Zip);

ええ、それはたくさんのフィールドですが、データベースレコードに保存したり、画面などに投げたりするだけです。ここでは、それほど多くの処理はありません。

パラメータリストが長くなると、フィールドに異なるデータ型を指定できるようになります。次のような関数が表示されるときのように:

void updateCustomer(String type, String status,
  int lastOrderNumber, int pastDue, int deliveryCode, int birthYear,
  int addressCode,
  boolean newCustomer, boolean taxExempt, boolean creditWatch,
  boolean foo, boolean bar);

そして、私はそれが呼ばれているのを見ます:

updateCustomer("A", "M", 42, 3, 1492, 1969, -7, true, false, false, true, false);

心配になります。呼び出しを見ると、これらすべての不可解な数字、コード、およびフラグが何を意味するのかはまったく明確ではありません。これは単にエラーを求めているだけです。プログラマーは、パラメーターの順序について簡単に混乱し、誤って2つを切り替える可能性があり、それらが同じデータ型であれば、コンパイラーはそれを受け入れるだけです。私はむしろこれらすべてが列挙型であるシグネチャを持っているので、呼び出しは「A」の代わりにType.ACTIVEや「false」の代わりにCreditWatch.NOなどを渡します。

7
Jay

私は賢明な亀裂のように聞こえたくありませんが、あなたが渡すデータを確実にチェックする必要もあります実際にコンストラクタに渡すもの(またはそのためのメソッド)物質)は、オブジェクトの動作に少し重点を置いたような匂いがします。

誤解しないでください:メソッドとコンストラクターwillには多くのパラメーターがあります。ただし、発生した場合は、代わりにdatabehaviorでカプセル化することを検討してください。

この種の匂い(リファクタリングについて話しているので、この恐ろしいWordが適切だと思われます...)は、多くの(読み取り:任意の)プロパティまたはゲッター/セッターを持つオブジェクトでも検出される場合があります。

7
Daren Thomas

コンストラクターのパラメーターの一部がオプションの場合、コンストラクターで必要なパラメーターを取得し、ビルダーを返すオプションのメソッドを持っているビルダーを使用するのが理にかなっています。

return new Shniz.Builder(foo, bar).baz(baz).quux(quux).build();

この詳細については、Effective Java、第2版、p。 11.メソッドの引数については、同じ本(p。189)がパラメーターリストを短縮するための3つのアプローチを説明しています。

  • メソッドを、より少ない引数を取る複数のメソッドに分割します
  • パラメーターのグループを表す静的ヘルパーメンバークラスを作成します。つまり、DinoDonkeyおよびdinoの代わりにdonkeyを渡します。
  • パラメーターがオプションの場合、上記のビルダーをメソッドに採用し、すべてのパラメーターのオブジェクトを定義し、必要なパラメーターを設定してから、そのメソッドでいくつかのexecuteメソッドを呼び出すことができます
5
Fabian Steeg

適切な回答を保証するのに十分な情報を提供していません。長いパラメーターリストは本質的に悪くはありません。

Shniz(foo、bar、baz、quux、fred、wilma、barney、dino、donkey)

次のように解釈できます:

void Shniz(int foo, int bar, int baz, int quux, int fred, 
           int wilma, int barney, int dino, int donkey) { ...

この場合、コンパイラーがチェックできるようにさまざまなパラメーターに意味を与え、視覚的にコードを読みやすくするため、パラメーターをカプセル化するクラスを作成する方がはるかに優れています。また、後で読みやすく、リファクタリングしやすくなります。

// old way
Shniz(1,2,3,2,3,2,1,2);
Shniz(1,2,2,3,3,2,1,2); 

//versus
ShnizParam p = new ShnizParam { Foo = 1, Bar = 2, Baz = 3 };
Shniz(p);

または、次の場合:

void Shniz(Foo foo, Bar bar, Baz baz, Quux quux, Fred fred, 
           Wilma wilma, Barney barney, Dino dino, Donkey donkey) { ...

これは、すべてのオブジェクトが異なるため(また、混乱する可能性が低いため)、まったく異なるケースです。すべてのオブジェクトが必要であり、それらがすべて異なる場合、パラメータークラスを作成することはほとんど意味がないことに同意しました。

また、一部のパラメーターはオプションですか?メソッドオーバーライド(同じメソッド名ですが、異なるメソッドシグネチャ)がありますか?これらの種類の詳細はすべて、best答えが何であるかに関して重要です。

*プロパティバッグも役立ちますが、背景が与えられていないため、特に優れているわけではありません。

ご覧のとおり、この質問には複数の正解があります。好きなものを選んでください。

4
Robert Paulson

デフォルトのコンストラクターとプロパティセッターを使用します。 C#3.0には、これを自動的に行うための素敵な構文があります。

return new Shniz { Foo = foo,
                   Bar = bar,
                   Baz = baz,
                   Quuz = quux,
                   Fred = fred,
                   Wilma = wilma,
                   Barney = barney,
                   Dino = dino,
                   Donkey = donkey
                 };

コードの改善は、コンストラクターを単純化し、さまざまな組み合わせをサポートするために複数のメソッドをサポートする必要がないことです。 「呼び出し」構文はまだ少し「冗長」ですが、実際にはプロパティセッターを手動で呼び出すよりも悪くはありません。

4
tvanfosson

パラメータを複数の意味のある構造体/クラスにグループ化することができます(可能な場合)。

3
Julien Hoarau

私は通常、構造体アプローチに傾いています。おそらく、これらのパラメーターの大部分は何らかの方法で関連しており、メソッドに関連する要素の状態を表していると思われます。

パラメータのセットを意味のあるオブジェクトにすることができない場合、それはおそらくShnizが多すぎることを示す兆候であり、リファクタリングにはメソッドを個別の懸念事項に分解する必要があります。

2
Andrzej Doyle

言語でサポートされている場合は、名前付きパラメーターを使用し、可能な限り多くのオプション(妥当なデフォルト)を使用します。

2
user54650

その数のパラメーターがある場合、メソッドの処理が多すぎる可能性があります。そのため、まずメソッドをいくつかの小さなメソッドに分割してこの問題に対処します。それでもパラメーターが多すぎる場合は、引数をグループ化するか、一部のパラメーターをインスタンスメンバーに変えてみてください。

大きいクラスよりも小さいクラス/メソッドを優先します。単一の責任原則を忘れないでください。

1
Brian Rasmussen

あなたが説明した方法が進むべき道だと思います。多くのパラメーターを持つメソッドや、将来さらに多くのパラメーターが必要になる可能性があるメソッドを見つけると、通常、説明したように、通過するShnizParamsオブジェクトを作成します。

1
Instantsoup

コンストラクターで一度に設定せずに、properties/settersで設定するのはどうですか? Processクラスなど、このアプローチを利用するいくつかの.NETクラスを見てきました。

        Process p = new Process();

        p.StartInfo.UseShellExecute = false;
        p.StartInfo.CreateNoWindow = true;
        p.StartInfo.RedirectStandardOutput = true;
        p.StartInfo.RedirectStandardError = true;
        p.StartInfo.FileName = "cmd";
        p.StartInfo.Arguments = "/c dir";
        p.Start();
1
Gant

ソースコード行の複雑さを交換できます。メソッド自体の処理量が多すぎる場合(スイスナイフ)、別のメソッドを作成してタスクを半分にしようとします。メソッドが単純な場合、必要なパラメーターが多すぎる場合は、いわゆるパラメーターオブジェクトを使用します。

1
Karl

名前付き引数は、長い(または短い!)パラメータリストを曖昧さをなくし、(コンストラクタの場合)クラスのプロパティが存在することを要求せずに不変にすることを可能にする(それらをサポートする言語を仮定して)良いオプションです部分的に構成された状態。

この種のリファクタリングを行う際に私が探す他のオプションは、独立したオブジェクトとしてより適切に処理される関連パラメータのグループです。前の回答のRectangleクラスを例として使用すると、x、y、height、widthのパラメーターを受け取るコンストラクターは、xとyをPointオブジェクトにファクタリングし、3つのパラメーターをRectangleのコンストラクターに渡すことができます。または、もう少し進んで2つのパラメーター(UpperLeftPoint、LowerRightPoint)にしますが、それはより根本的なリファクタリングになります。

1
Dave Sherohman

パラメーターをパラメーターオブジェクト(構造体)に移動するアプローチに同意します。ただし、すべてを1つのオブジェクトに固定するのではなく、他の関数が同様のパラメーターグループを使用しているかどうかを確認してください。パラメーターオブジェクトは、パラメーターのセットがそれらの関数全体で一貫して変化することが予想される複数の関数で使用される場合、より価値があります。いくつかのパラメーターのみを新しいパラメーターオブジェクトに配置した可能性があります。

1

それはあなたが持っている引数の種類に依存しますが、もしそれらが多くのブール値/オプションであるなら、多分あなたはFlag Enumを使うことができますか?

0
scottm

その問題は、クラスで解決しようとしている問題の領域に深く結びついていると思います。

場合によっては、7パラメーターコンストラクターが不適切なクラス階層を示すことがあります。その場合、上記で提案されたヘルパー構造体/クラスは通常は適切なアプローチですが、プロパティバッグである構造体のロードが発生する傾向があります有用なことは何もしません。 8引数のコンストラクターは、クラスが汎用性が高く汎用性が高いことを示している可能性があるため、実際に役立つには多くのオプションが必要です。その場合、クラスをリファクタリングするか、実際の複雑なコンストラクターを隠す静的コンストラクターを実装できます。 Shniz.NewBaz(foo、bar)は、実際に正しいパラメーターを渡して実際のコンストラクターを呼び出すことができます。

0
axel_c

1つの考慮事項は、オブジェクトが作成されると、どの値が読み取り専用になるかということです。

公に書き込み可能なプロパティは、おそらく構築後に割り当てることができます。

最終的に値はどこから来ますか?いくつかの値は、ライブラリによって維持されている一部の構成データまたはグローバルデータからの値である場合、真に外部のものである可能性があります。

この場合、外部使用からコンストラクターを隠し、そのためのCreate関数を提供できます。 create関数は真に外部の値を取得してオブジェクトを構築し、ライブラリでのみ使用可能なアクセサーを使用してオブジェクトの作成を完了します。

オブジェクトに完全な状態を与えるために7つ以上のパラメーターを必要とするオブジェクトがあり、すべてが実際に外部にあるのは本当に奇妙です。

0
AnthonyWJones

Clasに引数が多すぎるコンストラクタがある場合、通常、責任が多すぎることを示しています。おそらく、同じ機能を提供するために協力する別々のクラスに分割できます。

コンストラクターに多くの引数が本当に必要な場合は、Builderパターンが役立ちます。目標は、すべての引数をコンストラクターに渡すことです。そのため、その状態は最初から初期化され、必要に応じてクラスを不変にすることができます。

下記参照 :

public class Toto {
    private final String state0;
    private final String state1;
    private final String state2;
    private final String state3;

    public Toto(String arg0, String arg1, String arg2, String arg3) {
        this.state0 = arg0;
        this.state1 = arg1;
        this.state2 = arg2;
        this.state3 = arg3;
    }

    public static class TotoBuilder {
        private String arg0;
        private String arg1;
        private String arg2;
        private String arg3;

        public TotoBuilder addArg0(String arg) {
            this.arg0 = arg;
            return this;
        }
        public TotoBuilder addArg1(String arg) {
            this.arg1 = arg;
            return this;
        }
        public TotoBuilder addArg2(String arg) {
            this.arg2 = arg;
            return this;
        }
        public TotoBuilder addArg3(String arg) {
            this.arg3 = arg;
            return this;
        }

        public Toto newInstance() {
            // maybe add some validation ...
            return new Toto(this.arg0, this.arg1, this.arg2, this.arg3);
        }
    }

    public static void main(String[] args) {
        Toto toto = new TotoBuilder()
            .addArg0("0")
            .addArg1("1")
            .addArg2("2")
            .addArg3("3")
            .newInstance();
    }

}
0
Guillaume