メソッドチェーンを使用する場合、オブジェクトを再利用しますか、それとも作成しますか?


37

次のようなメソッドチェーンを使用する場合:

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();

次の2つのアプローチがあります。

  • 次のように、同じオブジェクトを再利用します。

    public Car PaintedIn(Color color)
    {
        this.Color = color;
        return this;
    }
  • 次のCarように、すべてのステップでタイプの新しいオブジェクトを作成します。

    public Car PaintedIn(Color color)
    {
        var car = new Car(this); // Clone the current object.
        car.Color = color; // Assign the values to the clone, not the original object.
        return car;
    }

最初のものは間違っていますか、それとも開発者の個人的な選択ですか?


彼の最初のアプローチは直観的/誤解を招くコードをすぐに引き起こす可能性があると思います。例:

// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);

// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();

// Would `specificModel` car be yellow or of neutral color? How would you guess that if
// `yellowCar` were in a separate method called somewhere else in code?

何かご意見は?


1
何が問題なのvar car = new Car(Brand.Ford, 12345, Color.Silver);ですか?
ジェームズ

12
@James伸縮コンストラクター、流れるようなパターンは、オプションのパラメーターと必須パラメーターを区別するのに役立ちます(オプションではない場合、コンストラクターの引数が必要な場合)。また、流theな文章はかなり読みやすいです。
ニムチンプスキー

8
@NimChimpskyは、古き良き(のC#)のプロパティに何が起こったのか、そして必要なフィールドがあるコンストラクタ-私は流暢APIを爆破していないということは、私は大ファンだが、それらは多くの場合、過剰に使用している
クリス・S

8
@ChrisSセッター(私はjavaから来ています)に依存している場合、オブジェクトを可変にする必要がありますが、これはしたくないかもしれません。また、Fluentを使用すると、より優れたインテリテキストが得られます。必要な思考は少なく、IDEがオブジェクトをほとんど構築します。
ニムチンプスキー

1
@NimChimpskyええ、私は流さはJavaの大きな飛躍であることがわかります
クリスS

回答:


41

私は置くところ流暢なAPIを、それが作成されたオブジェクトから自分の「ビルダー」クラス別々だし。そうすれば、クライアントがFluent APIを使用したくない場合でも手動で使用でき、ドメインオブジェクトを汚染することはありません(単一の責任原則に従って)。この場合、次のものが作成されます。

  • Car これはドメインオブジェクトです
  • CarBuilder 流れるようなAPIを保持する

使用法は次のようになります。

var car = CarBuilder.BuildCar()
    .OfBrand(Brand.Ford)
    .OfModel(12345)
    .PaintedIn(Color.Silver)
    .Build();

CarBuilderこのクラスは、(私はここではC#の命名規則を使用しています)、次のようになります。

public class CarBuilder {

    private Car _car;

    /// Constructor
    public CarBuilder() {
        _car = new Car();
        SetDefaults();
    }

    private void SetDefaults() {
        this.OfBrand(Brand.Ford);
          // you can continue the chaining for 
          // other default values
    }

    /// Starts an instance of the car builder to 
    /// build a new car with default values.
    public static CarBuilder BuildCar() {
        return new CarBuilder();
    }

    /// Sets the brand
    public CarBuilder OfBrand(Brand brand) {
        _car.SetBrand(brand);
        return this;
    }

    // continue with OfModel(...), PaintedIn(...), and so on...
    // that returns "this" to allow method chaining

    /// Returns the built car
    public Car Build() {
        return _car;
    }

}

このクラスはスレッドセーフではないことに注意してください(各スレッドには独自のCarBuilderインスタンスが必要です)。また、流れるようなAPIは本当にクールな概念ですが、単純なドメインオブジェクトを作成するためには多すぎると思われます。

この取り引きは、より抽象的なもの用のAPIを作成し、より複雑なセットアップと実行を行う場合に便利です。そのため、単体テストとDIフレームワークでうまく機能します。ウィキペディアのFluent Interface記事のJavaセクションで、永続性、日付処理、およびモックオブジェクトを使用した他の例を見ることができます。


編集:

コメントからわかるように。Builderクラスを静的な内部クラス(Car内)にし、Carを不変にすることができます。Carを不変にするこの例は少しばかげているように見えます。しかし、構築されたオブジェクトの内容を絶対に変更したくない、より複雑なシステムでは、変更したい場合があります。

以下は、静的内部クラスを実行する方法と、それが構築する不変オブジェクトの作成を処理する方法の1つの例です。

// the class that represents the immutable object
public class ImmutableWriter {

    // immutable variables
    private int _times; private string _write;

    // the "complex" constructor
    public ImmutableWriter(int times, string write) {
        _times = times;
        _write = write;
    }

    public void Perform() {
        for (int i = 0; i < _times; i++) Console.Write(_write + " ");
    }

    // static inner builder of the immutable object
    protected static class ImmutableWriterBuilder {

        // the variables needed to construct the immutable object
        private int _ii = 0; private string _is = String.Empty;

        public void Times(int i) { _ii = i; }

        public void Write(string s) { _is = s; }

        // The stuff is all built here
        public ImmutableWriter Build() {
            return new ImmutableWriter(_ii, _is);
        }

    }

    // factory method to get the builder
    public static ImmutableWriterBuilder GetBuilder() {
        return new ImmutableWriterBuilder();
    }
}

使用法は次のとおりです。

var writer = ImmutableWriter
                .GetBuilder()
                .Write("peanut butter jelly time")
                .Times(2)
                .Build();

writer.Perform();
// console writes: peanut butter jelly time peanut butter jelly time 

編集2:コメントのPeteは、複雑なドメインオブジェクトを使用した単体テストの作成のコンテキストでラムダ関数を使用したビルダーの使用に関するブログ投稿を行いました。ビルダーをもう少し表現力豊かにする興味深い代替手段です。

CarBuilder代わりにこのメソッドが必要な場合:

public static Car Build(Action<CarBuilder> buildAction = null) {
    var carBuilder = new CarBuilder();
    if (buildAction != null) buildAction(carBuilder);
    return carBuilder._car;
}

これとして使用できます:

Car c = CarBuilder
    .Build(car => 
        car.OfBrand(Brand.Ford)
           .OfModel(12345)
           .PaintedIn(Color.Silver);

3
@Baquetaこれは、josh blochの効果的なjavaの概要です
-NimChimpsky

6
@Baquetaは、Java開発者、imhoの読書が必要でした。
ニムチンプスキー

3
私見の大きな利点は、このパターンを(適切に変更した場合)使用して、完了していない構築中のオブジェクトのインスタンスがビルダーをエスケープしないようにすることができることです。たとえば、未定義の色を持つ車が存在しないことを確認できます。
スカーフリッジ

1
うーん...私は常に、ビルダーパターンの最終メソッドbuild()(またはBuild())を呼び出しましたCar()。ビルドする型の名前(例では)ではありません。また、Carが真に不変のオブジェクト(たとえば、そのすべてのフィールドがreadonly)である場合、ビルダーでさえもオブジェクトを変更できないため、Build()メソッドは新しいインスタンスの構築を担当します。これを行う1つの方法Carは、Builderを引数として取るコンストラクターを1つだけにすることです。その後、Build()メソッドはちょうどできますreturn new Car(this);
ダニエル・プライデン

1
ラムダに基づいてビルダーを作成する別のアプローチについてブログを書きました。投稿にはおそらく少し編集が必要でしょう。私のコンテキストは主に単体テストの範囲内でしたが、該当する場合は他の分野にも適用できます。ここで見つけることができます:petesdotnet.blogspot.com/2012/05/…–
ピート

9

場合によります。

あなたの車はエンティティまたは値オブジェクトですか?車がエンティティである場合、オブジェクトのアイデンティティが重要であるため、同じ参照を返す必要があります。オブジェクトが値オブジェクトの場合、不変である必要があります。つまり、唯一の方法は毎回新しいインスタンスを返すことです。

後者の例は、値オブジェクトである.NETのDateTimeクラスです。

var date1 = new DateTime(2012,1,1);
var date2 = date1.AddDays(1);
// date2 now refers to Jan 2., while date1 remains unchanged at Jan 1.

ただし、モデルがエンティティである場合、ビルダークラスを使用してオブジェクトを構築するというSpoikeの回答が気に入っています。言い換えれば、あなたが与えた例は、車が価値オブジェクトである場合にのみ意味があります。


1
「エンティティ」対「値」の質問に対して+1。クラスが可変型か不変型か(このオブジェクトを変更すべきかどうか)の問題であり、設計に影響しますが、完全にあなた次第です。メソッドが新しいオブジェクトを返さない限り、通常、メソッドチェーンが可変型で機能するとは期待しません。
ケーシークーボール

6

個別の静的インナービルダーを作成します。

必須パラメーターには通常のコンストラクター引数を使用します。オプションの流れるようなAPI。

メソッドNewCarInColourまたは同様の名前を変更しない限り、色を設定するときに新しいオブジェクトを作成しないでください。

私は彼が必要に応じてブランドと残りのオプションを使用してこのようなことをします(これはjavaですが、あなたのものはjavascriptのように見えますが、それらは少しのnitピッキングと互換性があります):

Car yellowMercedes = new Car.Builder(Brand.MercedesBenz).PaintedIn(Color.Yellow).create();

Car specificYellowModel =new Car.Builder(Brand.MercedesBenz).WithModel(99).PaintedIn(Color.Yellow).create();

4

最も重要なことは、選択した決定が何であれ、メソッド名やコメントに明確に記載されていることです。

標準はありません。メソッドは、新しいオブジェクトを返す場合があります(ほとんどのStringメソッドはそうする)か、チェーン目的またはメモリ効率のためにこのオブジェクトを返します。

私はかつて3Dベクトルオブジェクトを設計し、数学演算ごとに両方のメソッドを実装しました。インスタントスケールメソッド:

Vector3D scaleLocal(float factor){
    this.x *= factor; 
    this.y *= factor; 
    this.z *= factor; 
    return this;
}

Vector3D scale(float factor){
    Vector3D that = new Vector3D(this); // clone this vector
    return that.scaleLocal(factor);
}

3
+1。非常に良い点。どうしてこれが下票になったのか本当に分かりません。ただし、選択した名前はあまり明確ではないことに注意してください。私はそれらをscale(ミューテーター)およびscaledBy(ジェネレーター)と呼びます。
back2dos

良い点は、名前がより明確になっている可能性があることです。命名は、ライブラリから使用した他の数学クラスの規則に従いました。混乱を避けるため、メソッドのjavadocコメントにも効果が記載されています。
XGouchet

3

混乱を招くかもしれないと思ういくつかの問題がここにあります...質問の最初の行:

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();

コンストラクター(新規)とcreateメソッドを呼び出しています... create()メソッドはほとんどの場合、静的メソッドまたはビルダーメソッドであり、コンパイラーは警告またはエラーでキャッチして通知する必要があります。方法、この構文は間違っているか、いくつかのひどい名前があります。しかし、後で、両方を使用しないので、それを見てみましょう。

// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);

// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();

繰り返しますが、新しいコンストラクタを使用するのではなく、作成を使用します。ことは、代わりにcopy()メソッドを探していると思います。したがって、それが事実であり、それが単に貧しい名前である場合、1つのことを見てみましょう...あなたはmercedes.Paintedin(Color.Yellow).Copy()を呼び出します'コピーされる前に-ロジックの通常のフロー、私に。したがって、コピーを最初に置きます。

var yellowCar = mercedes.Copy().PaintedIn(Color.Yellow)

私には、あなたがコピーをペイントして黄色い車を作っていることが簡単にわかります。


newとCreate()の間の不協和音を指摘するための+1。
ジョシュアドレーク

1

最初のアプローチには、あなたが言及した欠点がありますが、ドキュメントでそれを明確にすれば、中途半端なコーダーには問題はないはずです。私が個人的に使用したメソッドチェーンコードはすべて、このように機能します。

2番目のアプローチには、明らかに作業量が多いという欠点があります。また、返すコピーが浅いコピーと深いコピーのどちらを実行するかを決定する必要があります。これはクラスごとに、またはメソッドごとに異なる場合があります。これが文字列のような不変オブジェクトの唯一のオプションであることは注目に値します。

あなたが何をするにしても、同じクラス内で混ぜたり合わせたりしないでください!


1

私はむしろ「拡張メソッド」メカニズムと同じように考えたいと思います。

public Car PaintedIn(this Car car, Color color)
{
    car.Color = color;
    return car;
}

0

これは上記の方法のバリエーションです。違いは、ビルダーのメソッド名と一致するCarクラスの静的メソッドがあるため、明示的にビルダーを作成する必要がないことです。

Car car = Car.builder().ofBrand(Brand.Ford).ofColor("Green")...

チェーンされたBuilder呼び出しで使用するものと同じメソッド名を使用できます。

Car car = Car.ofBrand(Brand.Ford).ofColor("Green")...

また、クラスには.copy()メソッドがあり、現在のインスタンスのすべての値が入力されたビルダーを返すため、テーマにバリエーションを作成できます。

Car red = car.copy().paintedIn("Red").build();

最後に、ビルダーの.build()メソッドは、必要なすべての値が提供されていることを確認し、欠落している場合はスローします。ビルダーのコンストラクターにいくつかの値を要求し、残りをオプションにすることをお勧めします。その場合、他の回答のパターンの1つが必要になります。

public enum Brand {
    Ford, Chrysler, GM, Honda, Toyota, Mercedes, BMW, Lexis, Tesla;
}

public class Car {
    private final Brand brand;
    private final int model;
    private final String color;

    public Car(Brand brand, int model, String color) {
        this.brand = brand;
        this.model = model;
        this.color = color;
    }

    public Brand getBrand() {
        return brand;
    }

    public int getModel() {
        return model;
    }

    public String getColor() {
        return color;
    }

    @Override public String toString() {
        return brand + " " + model + " " + color;
    }

    public Builder copy() {
        Builder builder = new Builder();
        builder.brand = brand;
        builder.model = model;
        builder.color = color;
        return builder;
    }

    public static Builder ofBrand(Brand brand) {
        Builder builder = new Builder();
        builder.brand = brand;
        return builder;
    }

    public static Builder ofModel(int model) {
        Builder builder = new Builder();
        builder.model = model;
        return builder;
    }

    public static Builder paintedIn(String color) {
        Builder builder = new Builder();
        builder.color = color;
        return builder;
    }

    public static class Builder {
        private Brand brand = null;
        private Integer model = null;
        private String color = null;

        public Builder ofBrand(Brand brand) {
            this.brand = brand;
            return this;
        }

        public Builder ofModel(int model) {
            this.model = model;
            return this;
        }

        public Builder paintedIn(String color) {
            this.color = color;
            return this;
        }

        public Car build() {
            if (brand == null) throw new IllegalArgumentException("no brand");
            if (model == null) throw new IllegalArgumentException("no model");
            if (color == null) throw new IllegalArgumentException("no color");
            return new Car(brand, model, color);
        }
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.