長いパラメーターリストを持つコンストラクターを使用せずに大きな不変オブジェクトを構築する


96

私はいくつかの大きな(3つ以上のフィールド)オブジェクトを持っていますが、それらは不変である必要があります。そのような場合に遭遇するたびに、長いパラメーターリストを使用してコンストラクターのabominationを作成する傾向があります。

違和感があり、使いづらく、読みやすさが低下します。

フィールドがリストのようなある種のコレクション型である場合はさらに悪いことです。simple addSibling(S s)はオブジェクトの作成を非常に簡単にしますが、オブジェクトを変更可能にします。

このような場合、皆さんは何を使用しますか?

私はScalaとJavaを使用していますが、言語がオブジェクト指向である限り、問題は言語にとらわれないものだと思います。

私が考えることができるソリューション:

  1. 「長いパラメーターリストによるコンストラクターの嫌悪感」
  2. ビルダーパターン

5
私はビルダーパターンがこれに対する最良かつ最も標準的なソリューションだと思います。
ザカリーライト、

@Zachary:あなたにジョシュア・ブロックによって、ここで説明した、唯一Builderパターンの特殊な形の作品提唱されている何drdobbs.com/java/208403883?pgno=2 私が呼び出しに関連する「流れるようなインターフェイス」という用語で行くことを好みますこの「ビルダーパターンの特殊な形式」(私の回答を参照)。
SyntaxT3rr0r 2010年

回答:


76

さて、あなたは一度作成すると読みやすく、不変のオブジェクトの両方が必要ですか?

流暢なインターフェイスが正しく完了したら役立つと思います。

これは次のようになります(完全に構成された例)。

final Foo immutable = FooFactory.create()
    .whereRangeConstraintsAre(100,300)
    .withColor(Color.BLUE)
    .withArea(234)
    .withInterspacing(12)
    .build();

ほとんどのJavaプログラマーが流暢なインターフェイスを間違って取得し、オブジェクトを構築するために必要なメソッドでオブジェクトを汚染するため、私は「正しく完了」を太字で記述しました。

コツは、build()メソッドのみが実際にFooを作成することです(したがって、Fooは不変である可能性があります)。

FooFactory.create()whereXXX(..)およびwithXXX(..)はすべて「何か他のもの」を作成します。

それ以外の何かはFooFactoryかもしれません。これを行う1つの方法を次に示します。

FooFactoryは次のようになります。

// Notice the private FooFactory constructor
private FooFactory() {
}

public static FooFactory create() {
    return new FooFactory();
}

public FooFactory withColor( final Color col ) {
    this.color = color;
    return this;
}

public Foo build() {
    return new FooImpl( color, and, all, the, other, parameters, go, here );
}

11
@ all:「FooImpl」の「Impl」の接尾辞について文句を言わないでください。このクラスはファクトリー内に隠されており、流暢なインターフェースを書いている人以外は誰もそれを見ることはありません。ユーザーが気にしているのは、彼が「Foo」を取得することだけです。「FooImpl」「FooPointlessNitpick」も呼び出すことができた;)
SyntaxT3rr0r

5
プリエンプティブな感じですか?;)あなたは過去にこれについていじめられてきました。:)
グレッグD

3
私が彼が言及している一般的なエラーは、人々が個別のを持つのではなく、オブジェクトに"withXXX"(etc)メソッド追加することであると私は信じFooますFooFactory
ディーンハーディング2010年

3
まだFooImpl8つのパラメーターを持つコンストラクターがあります。改善点は何ですか?
Mot、

4
このコードを呼び出さないimmutableでください。ファクトリーオブジェクトを再利用しているのではないかと心配しています。つまりFooFactory people = FooFactory.create().withType("person"); Foo women = people.withGender("female").build(); Foo talls = people.tallerThan("180m").build();talls今は女性だけが含まれることになります。これは、不変のAPIでは発生しません。
トーマスアーレ2014年

60

Scala 2.8では、名前付きパラメーターとデフォルトパラメーター、およびcopyケースクラスのメソッドを使用できました。ここにいくつかのサンプルコードがあります:

case class Person(name: String, age: Int, children: List[Person] = List()) {
  def addChild(p: Person) = copy(children = p :: this.children)
}

val parent = Person(name = "Bob", age = 55)
  .addChild(Person("Lisa", 23))
  .addChild(Person("Peter", 16))

31
Scala言語を発明するための+1。うん、それは評判システムの乱用ですが... aww ... Scalaが大好きなので、やらなければなりませんでした。:)
Malax

1
ああ、男...私はほとんど同じことを答えました!まあ、私は良い仲間です。:-)私は前にあなたの答えを見なかったのではないかと思います... <shrug>
ダニエルC.ソブラル

20

さて、これをScala 2.8で検討してください:

case class Person(name: String, 
                  married: Boolean = false, 
                  espouse: Option[String] = None, 
                  children: Set[String] = Set.empty) {
  def marriedTo(whom: String) = this.copy(married = true, espouse = Some(whom))
  def addChild(whom: String) = this.copy(children = children + whom)
}

scala> Person("Joseph").marriedTo("Mary").addChild("Jesus")
res1: Person = Person(Joseph,true,Some(Mary),Set(Jesus))

もちろん、これには問題があります。たとえば、とを作成espouseしてOption[Person]、2人が結婚するようにします。私はどちらかに頼ることなくそれを解決する方法を考えることはできませんprivate varおよび/またはprivateコンストラクタプラス工場。


11

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

オプション1

実装自体を変更可能にしますが、変更可能および不変に公開するインターフェースを分離します。これは、Swingライブラリデザインから取得されます。

public interface Foo {
  X getX();
  Y getY();
}

public interface MutableFoo extends Foo {
  void setX(X x);
  void setY(Y y);
}

public class FooImpl implements MutableFoo {...}

public SomeClassThatUsesFoo {
  public Foo makeFoo(...) {
    MutableFoo ret = new MutableFoo...
    ret.setX(...);
    ret.setY(...);
    return ret; // As Foo, not MutableFoo
  }
}

オプション2

アプリケーションに、事前定義された大きな不変オブジェクトのセット(構成オブジェクトなど)が含まれている場合は、Springフレームワークの使用を検討してください。


3
オプション1は賢い(ただし、あまり賢くない)ので、気に入っています。
ティモシー

4
私はこれを以前に行いましたが、私の意見では、オブジェクトはまだ変更可能であり、変更メソッドのみが「非表示」になっているため、良い解決策になるにはほど遠いです。多分私はこの主題についてあまりにもうるさいです...
マラックス

オプション1のバリアントは、ImmutableバリアントとMutableバリアントに別々のクラスを持つことです。これにより、インターフェースよりも安全性が向上します。おそらく、変更可能なインターフェースにキャストするだけでよいImmutableという名前の変更可能なオブジェクトをAPIに与えると、APIのコンシューマに嘘をつきます。個別のクラスアプローチでは、前後に変換するメソッドが必要です。JodaTime APIはこのパターンを使用します。DateTimeおよびMutableDateTimeを参照してください。
toolbear 2011

6

さまざまな種類の不変性があることを覚えておくと役立ちます。あなたの場合、私は「ポプシクル」不変性が本当にうまくいくと思います:

ポプシクルの不変性:気まぐれに追記型の不変性をわずかに弱めていると私は思います。初期化中にしばらく変更可能であり、その後永久に「凍結」されたオブジェクトまたはフィールドを想像できます。この種の不変性は、相互に循環参照する不変オブジェクト、またはディスクにシリアル化され、逆シリアル化の際に、逆シリアル化プロセス全体が完了するまで「流体」である必要がある不変オブジェクトに特に役立ちます。フローズン。

したがって、オブジェクトを初期化してから、何らかの「フリーズ」フラグを設定して、書き込み不可であることを示します。できれば、ミューテーションを関数の背後に隠して、関数がAPIを使用するクライアントに対して純粋であることを確認します。


1
反対票?なぜこれが良い解決策ではないのかコメントを残したい人はいますか?
ジュリエット

+1。clone()新しいインスタンスを導出するためにを使用することを示唆しているため、おそらく誰かがこれに反対しました。
finnw 2010

それはJavaでスレッドの安全性を損なう可能性があるので、または多分それはだった:java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5
finnw

欠点は、将来の開発者が「フリーズ」フラグを処理するように注意する必要があることです。ミューテーターメソッドが後で追加され、メソッドが凍結されていないことをアサートするのを忘れた場合、問題が発生する可能性があります。同様に、freeze()メソッドを呼び出す必要があるが、呼び出さない新しいコンストラクターが作成された場合、状況は醜くなります。
stalepretzel 2013

5

また、不変オブジェクトにミューテーター(addSiblingなど)のように見えるメソッドを公開させ、新しいインスタンスを返すようにすることもできます。それが不変のScalaコレクションが行うことです。

欠点は、必要以上にインスタンスを作成する可能性があることです。また、部分的に構築されたオブジェクトを処理したくない場合を除いて、中間の有効な構成が存在する場合にのみ適用できます(兄弟のないノードの場合がほとんどです)。

たとえば、宛先がまだないグラフエッジは、有効なグラフエッジではありません。


申し立ての欠点-必要以上のインスタンスを作成すること-はそれほど問題ではありません。オブジェクトの割り当ては非常に安価で、存続期間の短いオブジェクトのガベージコレクションも同様です。エスケープ分析がデフォルトで有効になっている場合、この種の「中間オブジェクト」はスタックに割り当てられる可能性が高く、文字通り何も作成する必要はありません。
gustafc 2010年

2
@gustafc:うん。Cliff Clickはかつて、リッチボックスヒッキーのClojure Ant Colonyシミュレーションを大きなボックス(864コア、768 GB RAM)の1つでベンチマークした方法のストーリーを語っていました。1秒あたりの一時的なごみ。GCは汗をかきさえしませんでした。
イェルクWミッターク

5

4つの可能性を検討します。

new Immutable(one, fish, two, fish, red, fish, blue, fish); /*1 */

params = new ImmutableParameters(); /*2 */
params.setType("fowl");
new Immutable(params);

factory = new ImmutableFactory(); /*3 */
factory.setType("fish");
factory.getInstance();

Immutable boringImmutable = new Immutable(); /* 4 */
Immutable lessBoring = boringImmutable.setType("vegetable");

私にとって、2、3、4はそれぞれ異なる状況に適応しています。最初のものは、OPで引用されている理由により、愛するのが難しいものであり、一般的に、クリープが発生し、リファクタリングが必要なデザインの症状です。

(2)としてリストしているのは、「ファクトリー」の背後に状態がない場合に適していますが、(3)は状態がある場合に選択する設計です。スレッドと同期について心配したくない場合、(3)ではなく(2)を使用していることに気づき、多くのオブジェクトの生成に対していくつかの高価なセットアップを償却することを心配する必要はありません。一方、(3)は、実際の作業が工場の建設に入るときに呼び出されます(SPIからのセットアップ、構成ファイルの読み取りなど)。

最後に、他の誰かの答えがオプション(4)に言及しましたが、ここには小さな不変オブジェクトがたくさんあり、好ましいパターンは古いものからニュースのものを取得することです。

私は「パターンファンクラブ」のメンバーではないことに注意してください。確かに、エミュレートする価値があるものもありますが、人々が名前と面白い帽子を与えると、彼らは自分の役に立たない人生を歩んでいるようです。


6
これはビルダーパターンです(オプション2)
Simon Nickerson

それは彼の不変オブジェクトを放出するファクトリ(「ビルダー」)オブジェクトではないでしょうか?
bmargulies

その区別はかなり意味論的に思われます。豆はどのように作られますか?それはビルダーとどう違うのですか?
カール

ビルダー(または他の多く)のJava Bean規則のパッケージを必要としません。
トム・ホーティン-タックライン

4

もう1つの潜在的なオプションは、構成可能なフィールドが少なくなるようにリファクタリングすることです。フィールドのグループが(ほとんど)互いに動作するだけの場合は、それらを独自の小さな不変オブジェクトにまとめます。この「小さい」オブジェクトのコンストラクタ/ビルダーは、この「大きい」オブジェクトのコンストラクタ/ビルダーと同様に、より管理しやすくなるはずです。


1
注:この回答の走行距離は、問題、コードベース、および開発者のスキルによって異なる場合があります。
カール

2

私はC#を使用していますが、これらは私のアプローチです。検討してください:

class Foo
{
    // private fields only to be written inside a constructor
    private readonly int i;
    private readonly string s;
    private readonly Bar b;

    // public getter properties
    public int I { get { return i; } }
    // etc.
}

オプション1.オプションのパラメーターを持つコンストラクター

public Foo(int i = 0, string s = "bla", Bar b = null)
{
    this.i = i;
    this.s = s;
    this.b = b;
}

例として使用されnew Foo(5, b: new Bar(whatever))ます。4.0より前のJavaまたはC#バージョンには対応していません。ただし、すべてのソリューションが言語に依存しないわけではないため、これは一例です。

オプション2.単一のパラメーターオブジェクトを取るコンストラクター

public Foo(FooParameters parameters)
{
    this.i = parameters.I;
    // etc.
}

class FooParameters
{
    // public properties with automatically generated private backing fields
    public int I { get; set; }
    public string S { get; set; }
    public Bar B { get; set; }

    // All properties are public, so we don't need a full constructor.
    // For convenience, you could include some commonly used initialization
    // patterns as additional constructors.
    public FooParameters() { }
}

使用例:

FooParameters fp = new FooParameters();
fp.I = 5;
fp.S = "bla";
fp.B = new Bar();
Foo f = new Foo(fp);`

3.0以降のC#では、オブジェクト初期化子構文を使用してこれをよりエレガントにしています(セマンティックは前の例と同等です)。

FooParameters fp = new FooParameters { I = 5, S = "bla", B = new Bar() };
Foo f = new Foo(fp);

オプション3:
このような膨大な数のパラメーターを必要としないようにクラスを再設計します。その責任を複数のクラスに分割できます。または、必要に応じて、コンストラクターではなく特定のメソッドにのみパラメーターを渡します。常に実行可能なわけではありませんが、実行可能な場合は、実行する価値があります。

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