Saat menggunakan metode chaining, apakah saya menggunakan kembali objek atau membuatnya?


37

Saat menggunakan metode chaining seperti:

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

mungkin ada dua pendekatan:

  • Gunakan kembali objek yang sama, seperti ini:

    public Car PaintedIn(Color color)
    {
        this.Color = color;
        return this;
    }
  • Buat objek jenis baru Cardi setiap langkah, seperti ini:

    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;
    }

Apakah yang pertama salah atau lebih merupakan pilihan pribadi pengembang?


Saya percaya bahwa dia pertama kali mendekati dapat dengan cepat menyebabkan kode intuitif / menyesatkan. Contoh:

// 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?

Adakah pikiran?


1
Ada apa dengan ini var car = new Car(Brand.Ford, 12345, Color.Silver);?
James

12
@James konstruktor teleskopik, pola fasih dapat membantu membedakan antara parameter opsional dan yang diperlukan (jika mereka diperlukan konstruktor, jika tidak opsional). Dan fasihnya agak enak dibaca.
NimChimpsky

8
@NimChimpsky apa yang terjadi pada properti kuno (untuk C #) yang baik, dan konstruktor yang memiliki bidang yang diperlukan - bukan saya peledakan API Lancar, saya penggemar berat tetapi sering digunakan berlebihan
Chris S

8
@ Chris jika Anda mengandalkan setter (saya dari java) Anda harus membuat objek Anda bisa berubah, yang mungkin tidak ingin Anda lakukan. Dan Anda juga mendapatkan intellitext yang lebih baik ketika menggunakan fasih - membutuhkan lebih sedikit pemikiran, ide hampir membangun objek Anda untuk Anda.
NimChimpsky

1
@NimChimpsky ya saya bisa melihat betapa fasih adalah lompatan besar ke depan untuk Java
Chris S

Jawaban:


41

Saya akan menempatkan api yang fasih ke kelas "builder" sendiri terpisah dari objek yang dibuatnya. Dengan begitu, jika klien tidak ingin menggunakan api lancar Anda masih dapat menggunakannya secara manual dan itu tidak mencemari objek domain (mengikuti prinsip tanggung jawab tunggal). Dalam hal ini yang berikut akan dibuat:

  • Car yang merupakan objek domain
  • CarBuilder yang memegang API lancar

Penggunaannya akan seperti ini:

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

The CarBuilderkelas akan terlihat seperti ini (saya menggunakan C konvensi # penamaan di sini):

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;
    }

}

Perhatikan bahwa kelas ini tidak akan aman utas (setiap utas membutuhkan turunan CarBuilder sendiri). Juga perhatikan bahwa, meskipun api fasih adalah konsep yang sangat keren, itu mungkin berlebihan untuk tujuan membuat objek domain sederhana.

Kesepakatan ini lebih berguna jika Anda membuat API untuk sesuatu yang jauh lebih abstrak dan memiliki pengaturan dan pelaksanaan yang lebih kompleks, itulah sebabnya ia bekerja sangat baik dalam pengujian unit dan kerangka kerja DI. Anda dapat melihat beberapa contoh lain di bawah bagian Java artikel wikipedia Fluent Interface dengan ketekunan, penanganan tanggal, dan objek tiruan.


EDIT:

Sebagaimana dicatat dari komentar; Anda bisa membuat kelas Builder kelas dalam statis (di dalam Mobil) dan Mobil bisa dibuat abadi. Contoh membiarkan mobil ini berubah tampaknya agak konyol; tetapi dalam sistem yang lebih kompleks, di mana Anda benar-benar tidak ingin mengubah konten objek yang dibangun, Anda mungkin ingin melakukannya.

Di bawah ini adalah salah satu contoh bagaimana melakukan kelas batin statis dan bagaimana menangani penciptaan objek abadi yang dibangun:

// 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();
    }
}

Penggunaannya adalah sebagai berikut:

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

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

Sunting 2: Pete dalam komentar membuat posting blog tentang menggunakan pembangun dengan fungsi lambda dalam konteks penulisan unit test dengan objek domain yang kompleks. Ini adalah alternatif yang menarik untuk membuat pembangun sedikit lebih ekspresif.

Jika CarBuilderAnda perlu memiliki metode ini sebagai gantinya:

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

Yang bisa digunakan seperti ini:

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

3
@ Baqueta ini diuraikan java efektif josh
bloch

6
@ Baqueta wajib membaca untuk java dev, imho.
NimChimpsky

3
IMHO keuntungan besar adalah, bahwa Anda dapat menggunakan pola ini (jika dimodifikasi dengan tepat) untuk mencegah contoh objek yang sedang dibangun yang tidak selesai keluar dari pembangun. Misalnya, Anda dapat memastikan bahwa tidak akan ada Mobil dengan Warna yang tidak ditentukan.
scarfridge

1
Hmm ... Saya selalu menyebut metode terakhir dari pola builder build()(atau Build()), bukan nama tipe yang dibuatnya ( Car()dalam contoh Anda). Juga, jika Carmerupakan objek yang benar-benar tidak dapat diubah (mis., Semua bidangnya readonly), maka bahkan pembangunnya tidak akan dapat mengubahnya, sehingga Build()metode menjadi bertanggung jawab untuk membuat instance baru. Salah satu cara untuk melakukan ini adalah Carmemiliki hanya satu konstruktor, yang mengambil Builder sebagai argumennya; maka Build()metode bisa saja return new Car(this);.
Daniel Pryden

1
Saya membuat blog tentang pendekatan berbeda untuk membuat pembangun berdasarkan lambdas. Posting mungkin perlu sedikit diedit. Konteks saya sebagian besar berada di dalam ruang lingkup unit test, tetapi bisa diterapkan ke area lain juga jika berlaku. Itu dapat ditemukan di sini: petesdotnet.blogspot.com/2012/05/…
Pete

9

Itu tergantung.

Apakah Mobil Anda Entitas atau Objek Nilai ? Jika mobil adalah suatu entitas, maka identitas objek sangat penting, jadi Anda harus mengembalikan referensi yang sama. Jika objek adalah objek nilai, itu harus tidak berubah, artinya satu-satunya cara adalah mengembalikan contoh baru setiap kali.

Contoh yang terakhir adalah kelas DateTime di .NET yang merupakan objek nilai.

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.

Namun jika modelnya adalah entitas, saya suka jawaban Spoike menggunakan kelas pembangun untuk membangun objek Anda. Dengan kata lain, contoh yang Anda berikan hanya masuk akal IMHO jika Mobil adalah objek nilai.


1
+1 untuk pertanyaan 'Entitas' vs 'Nilai'. Ini adalah pertanyaan apakah kelas Anda adalah tipe yang bisa berubah atau tidak berubah (haruskah objek ini diubah?), Dan sepenuhnya terserah Anda, meskipun itu akan memengaruhi desain Anda. Saya biasanya tidak mengharapkan metode chaining untuk bekerja pada tipe yang bisa berubah, kecuali metode mengembalikan objek baru.
Casey Kuball

6

Buat pembangun batin statis terpisah.

Gunakan argumen konstruktor normal untuk parameter yang diperlukan. Dan api fasih untuk opsional.

Jangan membuat objek baru saat mengatur warna, kecuali jika Anda mengubah nama metode NewCarInColour atau yang serupa.

Saya akan melakukan sesuatu seperti ini tanpa merek seperti yang diperlukan dan sisanya opsional (ini adalah java, tetapi Anda terlihat seperti javascript, tapi cukup yakin mereka dapat dipertukarkan dengan sedikit nit picking):

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

Yang paling penting adalah bahwa apa pun keputusan yang Anda pilih, itu jelas dinyatakan dalam nama metode dan / atau komentar.

Tidak ada standar, kadang-kadang metode akan mengembalikan objek baru (sebagian besar metode String melakukannya) atau akan mengembalikan objek ini untuk tujuan rantai atau untuk efisiensi memori).

Saya pernah merancang objek Vektor 3D dan untuk setiap operasi matematika saya menerapkan kedua metode. Untuk metode skala instan:

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. Poin yang sangat bagus. Saya tidak benar-benar mengerti mengapa ini mendapat downvote. Namun saya perhatikan, bahwa nama yang Anda pilih tidak begitu jelas. Saya akan memanggil mereka scale(mutator) dan scaledBy(generator).
back2dos

Poin baiknya, nama bisa lebih jelas. Penamaan mengikuti konvensi kelas matematika lain yang saya gunakan dari perpustakaan. Efeknya juga dinyatakan dalam komentar javadoc tentang metode untuk menghindari kebingungan.
XGouchet

3

Saya melihat beberapa masalah di sini yang saya pikir mungkin membingungkan ... Baris pertama Anda dalam pertanyaan:

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

Anda memanggil konstruktor (baru) dan metode buat ... Metode buat () hampir selalu berupa metode statis atau metode pembangun, dan kompiler harus menangkapnya dalam peringatan atau kesalahan untuk memberi tahu Anda, baik cara, sintaks ini salah atau memiliki beberapa nama yang mengerikan. Tetapi nanti, Anda tidak menggunakan keduanya, jadi mari kita lihat itu.

// 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();

Sekali lagi dengan buat, hanya saja tidak dengan konstruktor baru. Masalahnya, saya pikir Anda mencari metode copy () sebagai gantinya. Jadi jika itu yang terjadi, dan itu hanya nama yang buruk, mari kita lihat satu hal ... Anda memanggil mercedes. Diecat (Warna. Di Bawah). Salinan () - Seharusnya mudah untuk melihat itu dan mengatakan itu sedang dicat 'Sebelum disalin - hanya aliran logika yang normal, bagi saya. Jadi, letakkan salinannya terlebih dahulu.

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

bagi saya, mudah untuk melihat di sana bahwa Anda melukis salinannya, membuat mobil kuning Anda.


+1 untuk menunjukkan disonansi antara yang baru dan Buat ();
Joshua Drake

1

Pendekatan pertama memang memiliki kelemahan yang Anda sebutkan, tetapi selama Anda membuatnya jelas dalam dokumen, setiap pembuat kode yang tidak kompeten seharusnya tidak memiliki masalah. Semua kode metode-rantai saya pribadi telah bekerja dengan telah bekerja dengan cara ini.

Pendekatan kedua jelas memiliki kelemahan menjadi lebih banyak pekerjaan. Anda juga harus memutuskan apakah salinan yang Anda kembalikan akan melakukan salinan dangkal atau dalam: yang terbaik dapat bervariasi dari kelas ke kelas atau metode ke metode, sehingga Anda akan memperkenalkan inkonsistensi atau berkompromi pada perilaku terbaik. Perlu dicatat bahwa ini adalah satu-satunya pilihan untuk objek yang tidak berubah, seperti string.

Apa pun yang Anda lakukan, jangan campur dan mencocokkan dalam kelas yang sama!


1

Saya lebih suka berpikir seperti mekanisme "Metode Perpanjangan".

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

0

Ini adalah variasi dari metode di atas. Perbedaannya adalah bahwa ada metode statis pada kelas Mobil yang cocok dengan nama metode pada Builder, jadi Anda tidak perlu secara eksplisit membuat Builder:

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

Anda dapat menggunakan nama metode yang sama yang Anda gunakan pada panggilan pembangun dirantai:

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

Juga, ada metode .copy () di kelas yang mengembalikan pembangun yang diisi dengan semua nilai dari instance saat ini, sehingga Anda bisa membuat variasi pada tema:

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

Akhirnya, metode .build () dari builder memeriksa apakah semua nilai yang diperlukan telah disediakan dan dilemparkan jika ada yang hilang. Mungkin lebih baik untuk memerlukan beberapa nilai pada konstruktor pembangun dan membiarkan sisanya menjadi opsional; dalam hal ini, Anda ingin salah satu pola dalam jawaban yang lain.

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);
        }
    }
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.