web-dev-qa-db-ja.com

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

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

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?

何かご意見は?

37

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

  • ドメインオブジェクトであるCar
  • Fluent APIを保持するCarBuilder

使い方は次のようになります:

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インスタンスが必要です)。また、fluent apiは非常に優れたコンセプトですが、単純なドメインオブジェクトを作成する目的では多分、やり過ぎです。

この取引は、より抽象的なもののAPIを作成していて、セットアップと実行がより複雑な場合に役立ちます。そのため、ユニットテストやDIフレームワークでうまく機能します。他の例 Javaセクションの下 に、永続性、日付処理、およびモックオブジェクトを表示できます。


編集:

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

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

// 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);
41
Spoike

場合によります。

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

後者の例は、値オブジェクトである.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の答えが気に入っています。言い換えれば、Carが値オブジェクトである場合にのみ、あなたが与えたその例は私見に意味があります。

9
Pete

別の静的内部ビルダーを作成します。

必須パラメーターには通常のコンストラクター引数を使用します。そしてオプションのための流暢なAPI。

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

私は彼のブランドを必要に応じて、残りはオプションでこのようなことをします(これはJavaですが、あなたはjavascriptのように見えますが、それらが少しばかりのピッキングと交換可能であることを確認してください):

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

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

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

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

私はかつて3D Vectorオブジェクトを設計し、すべての数学演算に対して両方のメソッドを実装しました。即座にスケールする方法:

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);
}
4
XGouchet

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

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).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();

ただし、新しいコンストラクタではなく、createを使用します。ことは、あなたが代わりにcopy()メソッドを探していると思います。そのため、それが当てはまり、名前が不十分な場合は、1つのことを見てみましょう... mercedes.Paintedin(Color.Yellow).Copy()を呼び出します。 'コピーされる前-ロジックの通常のフローにすぎません。だから最初にコピーを置きます。

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

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

3
Drake Clarris

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

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

最初のアプローチにはあなたが言及する欠点がありますが、ドキュメントでそれを明らかにする限り、ハーフコンピテントコーダーは問題を抱えてはなりません。私が個人的に使用したメソッドチェーンコードはすべてこの方法で機能しました。

2番目のアプローチには、明らかに作業が増えるという欠点があります。また、返されるコピーが浅いコピーを実行するか、深いコピーを実行するかを決定する必要もあります。これはクラスごと、またはメソッドごとに異なる可能性が高いため、不整合を導入するか、最適な動作で妥協します。これは、文字列などの不変オブジェクトの唯一のオプションであることは注目に値します。

何をするにせよ、同じクラス内で混ぜたり、一致させたりしないでください。

1
vaughandroid

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

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);
        }
    }
}
0
David Conrad