web-dev-qa-db-ja.com

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

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

しかし、10個以上のパラメーターに依存するオブジェクトを構築し、最終的にビルダーパターンを介してこれらすべてのパラメーターの設定を終了するとどうなりますか?想像してみてください。あなたは、個人情報、仕事情報、友達情報、興味情報、教育情報などを含むPersonタイプのオブジェクトを作成します。これはもういいですが、どういうわけか、4つ以上の同じパラメータを設定しましたよね?この2つのケースが同じと見なされないのはなぜですか?

21
Narek

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

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

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

Builderパターンは、これらの問題の1つのみ、つまり、多くの引数を持つ関数呼び出しの保守性の問題に対処します。したがって、次のような関数呼び出し

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

これは、存在しないビルダーメソッドを呼び出すと爆発するので、少し余分な安全性を提供しますが、それ以外の場合は、コンストラクター呼び出しのコメントにはないものはありません。また、ビルダーを手動で作成するにはコードが必要です。コードが増えると、常にバグが増える可能性があります。

新しい値の型を定義するのが簡単な言語では、名前付き引数をシミュレートするにはmicrotyping/tiny typesを使用する方が良いことがわかりました。型が本当に小さいので名前が付けられていますが、結局はもっとたくさん入力することになります;-)

_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、…は、パラメータの意味を説明する自己記述的な名前である必要があり、多くの場合、パラメータ変数を与えます。名前付き引数のビルダーのイディオムと比較して、必要な実装ははるかに単純で、バグが含まれる可能性は低くなります。例(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()メソッドを介してさらに引数を提供するようユーザーに要求します。コーディングの労力は最小限ですが、正しいプログラムを書くことをより困難にします。

したがって、「多くのunnamedパラメータによりコードの問題を維持することが困難になる」ためのアプローチは数多くありますが、他の問題が残っています。

根本的な問題に近づく

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

コンストラクターが完全に構築された依存関係をパラメーターとして取るを使用するのが最も簡単で、多くの場合最良のアプローチですが、これは呼び出し元への依存関係を管理する責任を示しています。依存関係が独立したエンティティでない限り、理想的ではありませんドメインモデル。

代わりに(abstract)ファクトリーまたは完全な依存性注入フレームワークが代わりに使用されることもありますが、これらは大部分のユースケースでは過剰になる可能性があります。特に、これらの引数の多くが準グローバルオブジェクトまたはオブジェクトのインスタンス化間で変化しない構成値である場合にのみ、これらは引数の数を減らします。例えば。パラメータ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);
  }
}
_

アプリケーションによっては、依存関係マネージャーがすべてを提供できるため、ファクトリメソッドがほとんど引数を持たないゲームチェンジャーになる場合や、インスタンス化が複雑になり、明らかなメリットがないため、大量のコードになる場合があります。このようなファクトリーは、パラメーターを管理するよりも、インターフェースを具象型にマッピングする場合に非常に役立ちます。ただし、このアプローチは、流暢なインターフェイスで単に非表示にするのではなく、パラメータが多すぎるという根本的な問題に対処しようとします。

25
amon

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

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

一方、クラス、たとえばクラスの特定の属性がオプションである単純なDTOがある場合、ビルダーパターンを使用すると、上記のオブジェクトの作成が容易になります。

9
Andy

各パーツ、例えば個人情報、仕事情報(各雇用 "stint")、各友人などは、独自のオブジェクトに変換する必要があります。

更新機能を実装する方法を検討してください。ユーザーは追加新しい作業履歴を希望しています。ユーザーは、情報の他の部分や以前の作業履歴を変更したくないので、そのままにしてください。

情報が多い場合は、1つずつ情報を蓄積していくことは避けられません。人間の直感に基づいて(プログラマにとって使いやすくする)または使用パターンに基づいて(通常、どの情報が同時に更新されるか)、これらの要素を論理的にグループ化できます。

Builderパターンは、型付き引数のリスト(順序付けまたは非順序付け)を取り込む方法にすぎません。これを実際のコンストラクターに渡すことができます(またはコンストラクターがビルダーの取り込んだ引数を読み取ることができます)。

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

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

1
rwong