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));
明らかに、型名A
、B
、C
、...パラメータの意味を説明する自己文書名、あなたはパラメータ変数を与えるだろうと、多くの場合、同じ名前でなければなりません。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()
ません。正しいプログラムを書くことはより困難です。
そのため、「多くの名前のないパラメータはコードの問題を維持するのを難しくします」に対処する多くのアプローチがありますが、他の問題は残ります。
根本的な問題へのアプローチ
たとえば、テスト容易性の問題。単体テストを作成するとき、テストデータを挿入し、外部の副作用を持つ依存関係と操作をモックアウトするテスト実装を提供する機能が必要です。コンストラクタ内でクラスをインスタンス化するとき、私はそれを行うことができません。クラスの責任が他のオブジェクトの作成である場合を除き、重要なクラスをインスタンス化しないでください。これは、単一の責任問題と密接に関連しています。クラスの責任に焦点を当てるほど、テストが簡単になります(多くの場合、使いやすくなります)。
コンストラクターがパラメーターとして完全に構築された依存関係を取得するのが、最も簡単でしばしば最適なアプローチです。ただし、これは、依存関係がドメインモデルの独立したエンティティでない限り、呼び出し側への依存関係を管理する責任を負います。
時には(抽象的な)ファクトリーまたは完全な依存関係注入フレームワークが代わりに使用されますが、これらはほとんどのユースケースでは過剰な場合があります。特に、これらの引数の多くが準グローバルオブジェクトまたはオブジェクトのインスタンス化間で変化しない設定値である場合にのみ、これらは引数の数を減らします。例えばパラメータ場合a
とd
グローバルっぽいだったが、我々が得るだろう
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なインターフェイスでパラメーターを隠すのではなく、パラメーターが多すぎるという根本的な問題に対処しようとします。