多数のパラメーターとビルダーパターンを持つコンストラクター


21

クラスに多くのパラメーター(たとえば4個以上)を持つコンストラクターがある場合は、おそらくコードの匂いであることがよく知られています。クラスがSRPを満たすかどうかを再検討する必要があります。

しかし、10個以上のパラメーターに依存するオブジェクトをビルドし、オブジェクトを作成し、最終的にBuilderパターンを使用してこれらすべてのパラメーターを設定するとどうなりますか?Person個人情報、仕事情報、友人情報、興味情報、教育情報などを含むタイプオブジェクトを作成するとします。これは既に良いことですが、どういうわけか同じ4つ以上のパラメーターを設定していますか?なぜこの2つのケースは同じと見なされないのですか?

回答:


25

Builderパターンは、多くの引数の「問題」を解決しません。しかし、なぜ多くの議論に問題があるのでしょうか?

  • 彼らはあなたのクラスがやり過ぎかもしれないこと示しています。ただし、合理的にグループ化できない多くのメンバーを合法的に含む多くのタイプがあります。
  • 多くの入力を持つ関数のテストと理解は、文字通り、指数関数的にさらに複雑になります!
  • 言語が名前付きパラメーターを提供しない場合、関数呼び出しは自己文書化されません。7番目のパラメーターが何をするのかわからないため、多くの引数を使用して関数呼び出しを読み取ることは非常に困難です。特に動的に型付けされた言語を使用している場合、すべてが文字列である場合、または最後のパラメーターがtrue何らかの理由である場合は、5番目と6番目の引数が誤って入れ替わったかどうかさえ気付かないでしょう。

名前付きパラメーターの偽造

Builderパターンのアドレスこれらの問題は、多くの引数を持つ関数呼び出しのつまり保守性の懸念の一つだけ*。したがって、次のような関数呼び出し

MyClass o = new MyClass(a, b, c, d, e, f, g);

になるかもしれない

MyClass o = MyClass.builder()
  .a(a).b(b).c(c).d(d).e(e).f(f).g(g)
  .build();

∗ Builderパターンは、元々、複合オブジェクトを組み立てるための表現に依存しないアプローチとして意図されていました。これは、パラメーターの名前付き引数よりもはるかに大きな願望です。特に、ビルダーパターンは流れるようなインターフェイスを必要としません。

これは、存在しないビルダーメソッドを呼び出すと爆発するため、少し余分な安全性を提供しますが、コンストラクター呼び出しのコメントにはないものは何ももたらしません。また、ビルダーを手動で作成するにはコードが必要であり、より多くのコードに常により多くのバグを含めることができます。

新しい値型を定義するのが簡単な言語では、名前付き引数をシミュレートするためにマイクロタイピング/タイニー型を使用する方が良いことがわかりました。型は本当に小さいのでそう呼ばれていますが、あなたはもっとたくさん入力することになります;-)

MyClass o = new MyClass(
  new MyClass.A(a), new MyClass.B(b), new MyClass.C(c),
  new MyClass.D(d), new MyClass.E(e), new MyClass.F(f),
  new MyClass.G(g));

明らかに、型名ABC、...パラメータの意味を説明する自己文書名、あなたはパラメータ変数を与えるだろうと、多くの場合、同じ名前でなければなりません。builder-for-named-argumentsイディオムと比較して、必要な実装ははるかに単純であり、したがってバグが含まれる可能性は低くなります。例(Java風の構文):

class MyClass {
  ...
  public static class A {
    public final int value;
    public A(int a) { value = a; }
  }
  ...
}

コンパイラは、すべての引数が提供されたことを保証するのに役立ちます。Builderでは、欠落している引数を手動で確認するか、ステートマシンをホスト言語型システムにエンコードする必要があります。どちらにもバグが含まれている可能性があります。

名前付き引数をシミュレートする別の一般的なアプローチがあります。インラインクラス構文を使用してすべてのフィールドを初期化する単一の抽象パラメーターオブジェクトです。Javaの場合:

MyClass o = new MyClass(new MyClass.Arguments(){{ argA = a; argB = b; argC = c; ... }});

class MyClass {
  ...
  public static abstract class Arguments {
    public int argA;
    public String ArgB;
    ...
  }
}

ただし、フィールドを忘れることはありえますが、これは非常に言語固有のソリューションです(JavaScript、C#、およびCでの使用を見てきました)。

幸いなことに、コンストラクターはすべての引数を検証できますが、これはオブジェクトが部分的に構成された状態で作成された場合には当てはまりinit()ません。正しいプログラムを書くことはより困難です。

そのため、「多くの名前のないパラメータはコードの問題を維持するのを難しくします」に対処する多くのアプローチがありますが、他の問題は残ります。

根本的な問題へのアプローチ

たとえば、テスト容易性の問題。単体テストを作成するとき、テストデータを挿入し、外部の副作用を持つ依存関係と操作をモックアウトするテスト実装を提供する機能が必要です。コンストラクタ内でクラスをインスタンス化するとき、私はそれを行うことができません。クラスの責任が他のオブジェクトの作成である場合を除き、重要なクラスをインスタンス化しないでください。これは、単一の責任問題と密接に関連しています。クラスの責任に焦点を当てるほど、テストが簡単になります(多くの場合、使いやすくなります)。

コンストラクターがパラメーターとして完全に構築された依存関係取得するのが、最も簡単でしばしば最適なアプローチです。ただし、これは、依存関係がドメインモデルの独立したエンティティでない限り、呼び出し側への依存関係を管理する責任を負います。

時には(抽象的な)ファクトリーまたは完全な依存関係注入フレームワークが代わりに使用されますが、これらはほとんどのユースケースでは過剰な場合があります。特に、これらの引数の多くが準グローバルオブジェクトまたはオブジェクトのインスタンス化間で変化しない設定値である場合にのみ、これらは引数の数を減らします。例えばパラメータ場合adグローバルっぽいだったが、我々が得るだろう

Dependencies deps = new Dependencies(a, d);
...
MyClass o = deps.newMyClass(b, c, e, f, g);

class MyClass {
  MyClass(Dependencies deps, B b, C c, E e, F f, G g) {
    this.depA = deps.newDepA(b, c);
    this.depB = deps.newDepB(e, f);
    this.g = g;
  }
  ...
}

class Dependencies {
  private A a;
  private D d;
  public Dependencies(A a, D d) { this.a = a; this.d = d; }
  public DepA newDepA(B b, C c) { return new DepA(a, b, c); }
  public DepB newDepB(E e, F f) { return new DepB(d, e, f); }
  public MyClass newMyClass(B b, C c, E e, F f, G g) {
    return new MyClass(deps, b, c, e, f, g);
  }
}

アプリケーションに応じて、これは依存関係マネージャーによってすべて提供できるため、ファクトリメソッドが引数をほとんど持たないゲームチェンジャーかもしれません。このようなファクトリーは、パラメーターを管理するよりも、インターフェースを具象型にマッピングする方がはるかに便利です。ただし、このアプローチでは、かなり流fluentなインターフェイスでパラメーターを隠すのではなく、パラメーターが多すぎるという根本的な問題に対処しようとします。


私は本当に自己文書化の部分に反対します。適切なIDE +適切なコメントがある場合、ほとんどの場合、コンストラクターとパラメーター定義は1回のキーストロークで完了します。
JavierIEH

Android Studio 3.0では、コンストラクターのパラメーター名は、コンストラクター呼び出しで渡される値の隣に表示されます。例:new A(operand1:34、operand2:56); operand1およびoperand2は、コンストラクター内のパラメーター名です。IDEによって表示され、コードが読みやすくなります。そのため、パラメーターを確認するために定義に移動する必要はありません。
ガーネット

9

Builderパターンは何も解決せず、設計エラーを修正しません。

10個のパラメーターを構築する必要があるクラスがある場合、それを構築するビルダーを作成しても、設計が突然改善されることはありません。問題のクラスのリファクタリングを選択する必要があります。

一方、クラスの特定の属性がオプションであるクラス(おそらく単純なDTO)がある場合、ビルダーパターンを使用すると、前述のオブジェクトの構築が容易になります。


1

個人情報、仕事情報(各雇用 "スティント")、各友人などの各部分は、独自のオブジェクトに変換する必要があります。

更新機能の実装方法を検討してください。ユーザーが新しい作業履歴を追加したい。ユーザーは、情報の他の部分も以前の作業履歴も変更したくない-それらを同じに保ちます。

大量の情報がある場合、一度に1つずつ情報を作成することは避けられません。これらの断片を論理的にグループ化することができます-人間の直感(プログラマーにとって使いやすくなります)または使用パターン(通常どの情報が同時に更新されるか)に基づいて。

Builderパターンは、型指定された引数のリスト(順序付きまたは順序なし)をキャプチャする方法であり、実際のコンストラクターに渡すことができます(または、コンストラクターは、ビルダーのキャプチャした引数を読み取ることができます)。

4のガイドラインは単なる経験則です。4つ以上の引数を必要とする状況があり、それらをグループ化する正気または論理的な方法はありません。そのような場合、struct直接またはプロパティセッターを使用して作成できるを作成するのが理にかなっています。structこの場合のとビルダーは非常に似た目的を果たすことに注意してください。

ただし、この例では、それらをグループ化するための論理的な方法をすでに説明しています。これが当てはまらない状況に直面している場合は、別の例で説明できます。

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