Mengapa kita membutuhkan kelas Builder ketika menerapkan pola Builder?


31

Saya telah melihat banyak implementasi pola Builder (terutama di Jawa). Semua dari mereka memiliki kelas entitas (katakanlah Personkelas), dan kelas pembangun PersonBuilder. Pembangun "menumpuk" berbagai bidang dan mengembalikan a new Persondengan argumen yang diteruskan. Mengapa kita secara eksplisit membutuhkan kelas builder, alih-alih menempatkan semua metode builder di Personkelas itu sendiri?

Sebagai contoh:

class Person {

  private String name;
  private Integer age;

  public Person() {
  }

  Person withName(String name) {
    this.name = name;
    return this;
  }

  Person withAge(int age) {
    this.age = age;
    return this;
  }
}

Saya hanya bisa mengatakannya Person john = new Person().withName("John");

Mengapa perlu PersonBuilderkelas?

Satu-satunya manfaat yang saya lihat, adalah kita dapat mendeklarasikan Personbidang sebagai final, sehingga memastikan kekekalan.


4
Catatan: Nama normal untuk pola ini adalah chainable setters: D
Mooing Duck

4
Prinsip tanggung jawab tunggal. Jika Anda memasukkan logika di kelas Person maka ada lebih dari satu pekerjaan yang harus dilakukan
reggaeguitar

1
Keuntungan Anda dapat diperoleh dengan cara apa pun: Saya dapat withNamemengembalikan salinan Orang hanya dengan bidang nama yang diubah. Dengan kata lain, Person john = new Person().withName("John");bisa bekerja walaupun Persontidak dapat diubah (dan ini adalah pola umum dalam pemrograman fungsional).
Brian McCutchon

9
@ MoingDuck: istilah umum lain untuk itu adalah antarmuka yang lancar .
Mac

2
@ Mac Saya pikir antarmuka yang fasih sedikit lebih umum - Anda menghindari voidmetode. Jadi, misalnya jika Personmemiliki metode yang mencetak nama mereka, Anda masih dapat menghubungkannya dengan Antarmuka Lancar person.setName("Alice").sayName().setName("Bob").sayName(). Ngomong-ngomong, saya melakukan anotasi pada JavaDoc dengan saran Anda @return Fluent interface- itu generik dan cukup jelas ketika itu berlaku untuk metode apa pun yang dilakukan return thispada akhir pelaksanaannya dan itu cukup jelas. Jadi, Builder juga akan melakukan antarmuka yang lancar.
VLAZ

Jawaban:


27

Ini agar Anda bisa berubah dan mensimulasikan parameter bernama pada saat yang sama.

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

Itu membuat sarung tangan Anda lepas dari orang itu sampai keadaan diatur dan, setelah diatur, tidak akan membiarkan Anda mengubahnya, namun setiap bidang diberi label dengan jelas. Anda tidak dapat melakukan ini hanya dengan satu kelas di Jawa.

Sepertinya Anda sedang berbicara tentang Josh Blochs Builder Pattern . Ini tidak harus bingung dengan Pola Geng Empat Pembangun . Ini adalah binatang yang berbeda. Mereka berdua menyelesaikan masalah konstruksi, tetapi dengan cara yang cukup berbeda.

Tentu saja Anda dapat membangun objek Anda tanpa menggunakan kelas lain. Tetapi kemudian Anda harus memilih. Anda kehilangan kemampuan untuk mensimulasikan parameter bernama dalam bahasa yang tidak memilikinya (seperti Java) atau Anda kehilangan kemampuan untuk tetap abadi sepanjang objek seumur hidup.

Contoh abadi, tidak memiliki nama untuk parameter

Person p = new Person("Arthur Dent", 42);

Di sini Anda membangun semuanya dengan satu konstruktor sederhana. Ini akan membuat Anda tetap tidak berubah tetapi Anda kehilangan simulasi parameter bernama. Itu sulit dibaca dengan banyak parameter. Komputer tidak peduli tetapi sulit bagi manusia.

Contoh simulasi bernama parameter dengan setter tradisional. Tidak kekal.

Person p = new Person();
p.name("Arthur Dent");
p.age(42);

Di sini Anda sedang membangun segalanya dengan setter dan mensimulasikan parameter bernama tetapi Anda tidak lagi dapat diubah. Setiap penggunaan setter mengubah status objek.

Jadi yang Anda dapatkan dengan menambahkan kelas adalah Anda bisa melakukan keduanya.

Validasi dapat dilakukan di build()jika kesalahan runtime untuk bidang usia yang hilang sudah cukup untuk Anda. Anda dapat memutakhirkan itu dan menegakkan yang age()disebut dengan kesalahan kompiler. Hanya tidak dengan pola pembangun Josh Bloch.

Untuk itu Anda memerlukan Bahasa Spesifik Domain internal (iDSL).

Ini memungkinkan Anda menuntut mereka menelepon age()dan name()sebelum menelepon build(). Tetapi Anda tidak dapat melakukannya hanya dengan kembali thissetiap kali. Setiap hal yang mengembalikan mengembalikan hal yang berbeda yang memaksa Anda untuk memanggil hal berikutnya.

Gunakan mungkin terlihat seperti ini:

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

Tapi ini:

Person p = personBuilder
    .age(42)
    .build()
;

menyebabkan kesalahan kompiler karena age()hanya valid untuk memanggil tipe yang dikembalikan oleh name().

IDSL ini sangat kuat ( JOOQ atau Java8 Streams misalnya) dan sangat baik untuk digunakan, terutama jika Anda menggunakan IDE dengan penyelesaian kode, tetapi itu adalah pekerjaan yang cukup adil untuk diatur. Saya akan merekomendasikan menyimpannya untuk hal-hal yang akan memiliki sedikit kode sumber tertulis terhadap mereka.


3
Dan kemudian seseorang bertanya mengapa Anda harus memberikan nama sebelum usia, dan satu-satunya jawaban adalah "karena ledakan kombinatorial". Saya cukup yakin orang dapat menghindari itu dalam sumber jika seseorang memiliki template yang tepat seperti C ++, atau kembali menggunakan tipe yang berbeda untuk semuanya.
Deduplicator

1
Sebuah abstraksi bocor membutuhkan pengetahuan tentang kompleksitas yang mendasari untuk dapat mengetahui bagaimana menggunakannya. Itu yang saya lihat di sini. Setiap kelas yang tidak tahu cara membangun itu sendiri harus digabungkan dengan Builder yang memiliki dependensi lebih lanjut (seperti tujuan dan sifat konsep Builder). Suatu kali (bukan di band camp) kebutuhan sederhana untuk membuat instance objek diperlukan 3 bulan untuk membatalkan hanya frack cluster. Aku tidak membohongimu.
radarbob

Salah satu keuntungan dari kelas yang tidak mengetahui aturan konstruksi mereka adalah ketika aturan itu berubah mereka tidak peduli. Saya hanya bisa menambahkan AnonymousPersonBuilderyang memiliki seperangkat aturan sendiri.
candied_orange

Kelas / desain sistem jarang menunjukkan aplikasi yang dalam "memaksimalkan kohesi dan meminimalkan kopling." Legion desain dan runtime bisa diperluas, dan kekurangan hanya bisa diperbaiki jika maksim dan pedoman OO diterapkan dengan perasaan fraktal, atau "itu kura-kura, semua jalan turun."
radarbob

1
@radarbob Kelas yang dibangun masih tahu bagaimana membangunnya sendiri. Konstruktornya bertanggung jawab untuk memastikan integritas objek. Kelas pembangun hanyalah penolong untuk konstruktor, karena konstruktor sering mengambil sejumlah besar parameter, yang tidak membuat API yang sangat bagus.
ReinstateMonicaSackTheStaff

57

Mengapa menggunakan / menyediakan kelas pembangun:

  • Untuk membuat objek abadi - manfaat yang telah Anda identifikasi. Berguna jika konstruksi membutuhkan beberapa langkah. FWIW, imutabilitas harus dilihat sebagai alat yang signifikan dalam upaya kami untuk menulis program yang dapat dirawat dan bebas bug.
  • Jika representasi runtime dari objek final (mungkin tidak berubah) dioptimalkan untuk membaca dan / atau penggunaan ruang, tetapi tidak untuk pembaruan. String dan StringBuilder adalah contoh yang bagus di sini. String yang berulang kali digabungkan tidak terlalu efisien, sehingga StringBuilder menggunakan representasi internal yang berbeda yang baik untuk ditambahkan - tetapi tidak sebagus penggunaan ruang, dan tidak sebaik untuk membaca dan menggunakan sebagai kelas String biasa.
  • Untuk dengan jelas memisahkan objek yang dibangun dari objek yang sedang dibangun. Pendekatan ini membutuhkan transisi yang jelas dari pembangunan ke konstruksi. Bagi konsumen, tidak ada cara untuk membingungkan objek yang sedang dibangun dengan objek yang dibangun: sistem tipe akan menegakkan ini. Itu berarti kadang-kadang kita dapat menggunakan pendekatan ini untuk "jatuh ke dalam lubang kesuksesan", seolah-olah, dan, ketika membuat abstraksi untuk orang lain (atau diri kita sendiri) untuk digunakan (seperti API atau layer), ini bisa menjadi sangat baik benda.

4
Mengenai poin terakhir. Jadi idenya adalah a PersonBuildertidak memiliki getter, dan satu-satunya cara untuk memeriksa nilai saat ini adalah dengan menelepon .Build()untuk mengembalikan a Person. Dengan cara ini. Build dapat memvalidasi objek yang dibangun dengan benar, benar? Apakah ini mekanisme yang dimaksudkan untuk mencegah penggunaan objek "sedang dibangun"?
AaronLS

@ AaronLS, ya, tentu saja; validasi kompleks dapat dimasukkan ke dalam Buildoperasi untuk mencegah objek buruk dan / atau kurang dibangun.
Erik Eidt

2
Anda juga dapat memvalidasi objek Anda di dalam konstruktornya, preferensi mungkin bersifat pribadi atau tergantung dari kerumitannya. Apa pun yang dipilih, intinya adalah bahwa sekali Anda memiliki objek abadi Anda, Anda tahu itu dibangun dengan benar dan tidak memiliki nilai yang tidak terduga. Objek abadi dengan keadaan yang salah seharusnya tidak terjadi.
Walfrat

2
@ Harun seorang pembangun dapat memiliki getter; itu bukan intinya. Tidak perlu, tetapi jika pembangun memiliki getter, Anda harus menyadari bahwa memanggil getAgepembangun dapat kembali nullketika properti ini belum ditentukan. Sebaliknya, Personkelas dapat memberlakukan invarian bahwa usia tidak pernah dapat null, yang tidak mungkin ketika pembangun dan objek aktual dicampur, seperti dengan contoh OP new Person() .withName("John"), yang dengan senang hati menciptakan Personusia tanpa usia. Ini berlaku bahkan untuk kelas yang bisa berubah, karena seter dapat memberlakukan invarian, tetapi tidak dapat menegakkan nilai awal.
Holger

1
@Holger poin Anda benar, tetapi terutama mendukung argumen bahwa Builder harus menghindari getter.
user949300

21

Salah satu alasannya adalah untuk memastikan bahwa semua data yang lewat mengikuti aturan bisnis.

Contoh Anda tidak mempertimbangkan ini, tetapi katakanlah seseorang melewati string kosong, atau string yang terdiri dari karakter khusus. Anda ingin melakukan semacam logika berdasarkan memastikan bahwa nama mereka sebenarnya nama yang valid (yang sebenarnya merupakan tugas yang sangat sulit).

Anda dapat menempatkan itu semua di kelas Person Anda, terutama jika logikanya sangat kecil (misalnya, hanya memastikan bahwa suatu usia adalah non-negatif) tetapi ketika logikanya tumbuh masuk akal untuk memisahkannya.


7
Contoh dalam pertanyaan ini memberikan pelanggaran aturan sederhana: tidak ada usia yang diberikan untukjohn
Caleth

6
+1 Dan sangat sulit untuk melakukan aturan untuk dua bidang secara bersamaan tanpa kelas pembangun (bidang X + Y tidak boleh lebih besar dari 10).
Reginald Blue

7
@ ReginaldBlue lebih sulit lagi jika mereka terikat oleh "x + y adalah genap". Kondisi awal kami dengan (0,0) adalah OK, Negara (1,1) juga OK, tetapi tidak ada urutan pembaruan variabel tunggal yang keduanya membuat status tetap valid dan membuat kami dari (0,0) hingga (1) , 1).
Michael Anderson

Saya percaya ini adalah keuntungan terbesar. Yang lainnya ramah.
Giacomo Alzetta

5

Sedikit sudut berbeda dalam hal ini dari apa yang saya lihat di jawaban lain.

The withFooPendekatan di sini adalah bermasalah karena mereka berperilaku seperti setter tetapi didefinisikan dengan cara yang membuatnya tampak mendukung kelas kekekalan. Di kelas Java, jika metode memodifikasi properti, itu kebiasaan untuk memulai metode dengan 'set'. Saya tidak pernah menyukainya sebagai standar tetapi jika Anda melakukan sesuatu yang lain itu akan mengejutkan orang dan itu tidak baik. Ada cara lain yang dapat Anda gunakan untuk mendukung perubahan dengan API dasar yang Anda miliki di sini. Sebagai contoh:

class Person {
  private final String name;
  private final Integer age;

  private Person(String name, String age) {
    this.name = name;
    this.age = age;
  }  

  public Person() {
    this.name = null;
    this.age = null;
  }

  Person withName(String name) {
    return new Person(name, this.age);
  }

  Person withAge(int age) {
    return new Person(this.name, age);
  }
}

Itu tidak memberikan banyak cara mencegah objek yang dibangun secara tidak benar tetapi mencegah perubahan pada objek yang ada. Mungkin konyol untuk hal semacam ini (dan begitu juga JB Builder). Ya, Anda akan membuat lebih banyak objek tetapi ini tidak semahal yang Anda bayangkan.

Sebagian besar Anda akan melihat pendekatan semacam ini digunakan dengan struktur data bersamaan seperti CopyOnWriteArrayList . Dan ini mengisyaratkan mengapa imutabilitas itu penting. Jika Anda ingin membuat utas kode Anda aman, ketidakberubahan harus selalu dipertimbangkan. Di Jawa, setiap utas diizinkan untuk menyimpan cache lokal dari keadaan variabel. Agar satu utas dapat melihat perubahan yang dilakukan pada utas lainnya, blok yang disinkronkan atau fitur konkurensi lainnya perlu digunakan. Semua ini akan menambah beberapa overhead ke kode. Tetapi jika variabel Anda final, tidak ada hubungannya. Nilai akan selalu menjadi apa yang diinisialisasi dan oleh karena itu semua utas melihat hal yang sama tidak peduli apa.


2

Alasan lain yang belum disebutkan secara eksplisit di sini, adalah bahwa build()metode ini dapat memverifikasi bahwa semua bidang isian berisi nilai yang valid (baik ditetapkan secara langsung, atau diturunkan dari nilai lain dari bidang lain), yang mungkin merupakan mode kegagalan paling mungkin yang sebaliknya akan terjadi.

Manfaat lain adalah bahwa Personobjek Anda pada akhirnya akan memiliki masa hidup yang lebih sederhana dan serangkaian invarian yang sederhana. Anda tahu bahwa begitu Anda memiliki Person p, Anda memiliki p.namedan valid p.age. Tak satu pun dari metode Anda yang harus dirancang untuk menangani situasi seperti "baik bagaimana jika usia ditetapkan tetapi bukan nama, atau bagaimana jika nama ditetapkan tetapi bukan usia?" Ini mengurangi kompleksitas keseluruhan kelas.


"[...] dapat memverifikasi bahwa semua bidang diatur [...]" Maksud dari pola Builder adalah bahwa Anda tidak harus mengatur sendiri semua bidang dan Anda dapat kembali ke default yang masuk akal. Jika Anda tetap harus mengatur semua bidang, mungkin Anda sebaiknya tidak menggunakan pola itu!
Vincent Savard

@VincentSavard Memang, tetapi dalam perkiraan saya, itulah penggunaan yang paling umum. Untuk memvalidasi bahwa beberapa set nilai "inti" telah ditetapkan, dan lakukan beberapa perlakuan mewah di sekitar beberapa nilai opsional (menetapkan default, memperoleh nilainya dari nilai lain, dll.).
Alexander - Reinstate Monica

Alih-alih mengatakan 'verifikasi bahwa semua bidang diatur', saya akan mengubah ini menjadi 'memverifikasi bahwa semua bidang berisi nilai yang valid [kadang-kadang tergantung pada nilai-nilai bidang lain]' (mis. Suatu bidang hanya dapat memungkinkan nilai-nilai tertentu tergantung pada nilai dari bidang lain). Ini dapat dilakukan di setiap penyetel, tetapi lebih mudah bagi seseorang untuk mengatur objek tanpa pengecualian yang dilemparkan sampai objek selesai dan siap untuk dibangun.
yitzih

Konstruktor kelas yang dibangun pada akhirnya bertanggung jawab atas validitas argumennya. Validasi dalam pembangun paling tidak terbaik, dan dapat dengan mudah keluar dari sinkronisasi dengan persyaratan konstruktor.
ReinstateMonicaSackTheStaff

@TKK Memang. Tetapi mereka memvalidasi hal-hal yang berbeda. Dalam banyak kasus saya telah menggunakan Builder di, tugas pembangun adalah untuk membangun input yang akhirnya diberikan kepada konstruktor. Misalnya pembangun dikonfigurasikan dengan a URI, a File, atau FileInputStream, dan menggunakan apa pun yang disediakan untuk memperoleh FileInputStreamyang akhirnya masuk ke panggilan konstruktor sebagai argumen
Alexander - Reinstate Monica

2

Builder juga dapat didefinisikan untuk mengembalikan antarmuka atau kelas abstrak. Anda bisa menggunakan pembangun untuk mendefinisikan objek, dan pembangun dapat menentukan subkelas beton apa yang akan dikembalikan berdasarkan sifat apa yang ditetapkan, atau apa yang ditetapkan, misalnya.


2

Pola pembangun digunakan untuk membangun / membuat objek langkah demi langkah dengan mengatur properti dan ketika semua bidang yang diperlukan diatur, lalu mengembalikan objek akhir menggunakan metode build. Objek yang baru dibuat tidak berubah. Poin utama yang perlu diperhatikan di sini adalah bahwa objek hanya dikembalikan ketika metode build terakhir dipanggil. Ini memastikan bahwa semua properti diatur ke objek dan dengan demikian objek tidak dalam keadaan tidak konsisten ketika dikembalikan oleh kelas pembangun.

Jika kita tidak menggunakan kelas builder dan langsung menempatkan semua metode kelas builder ke kelas Person itu sendiri, maka kita harus terlebih dahulu membuat Objek dan kemudian memanggil metode setter pada objek yang dibuat yang akan mengarah pada keadaan objek yang tidak konsisten antara penciptaan. objek dan pengaturan properti.

Jadi dengan menggunakan kelas builder (yaitu beberapa entitas eksternal selain kelas Person itu sendiri) kami memastikan bahwa objek tidak akan pernah berada dalam keadaan tidak konsisten.


@ Apakah Anda mengerti maksud saya? Penekanan utama yang saya coba taruh di sini adalah pada keadaan yang tidak konsisten. Dan itu adalah salah satu keuntungan utama menggunakan pola Builder.
Shubham Bondarde

2

Gunakan kembali objek pembangun

Seperti yang telah disebutkan orang lain, kekekalan dan verifikasi logika bisnis dari semua bidang untuk memvalidasi objek adalah alasan utama untuk objek pembangun terpisah.

Namun penggunaan kembali adalah manfaat lain. Jika saya ingin instantiate banyak objek yang sangat mirip, saya bisa membuat perubahan kecil pada objek builder dan melanjutkan instantiating. Tidak perlu membuat ulang objek pembangun. Penggunaan kembali ini memungkinkan pembangun untuk bertindak sebagai templat untuk membuat banyak objek abadi. Ini adalah manfaat kecil, tetapi bisa bermanfaat.


1

Sebenarnya, Anda dapat memiliki metode pembangun di kelas Anda sendiri, dan masih memiliki kekekalan. Itu hanya berarti metode pembangun akan mengembalikan objek baru, alih-alih memodifikasi yang sudah ada.

Ini hanya berfungsi jika ada cara untuk mendapatkan objek awal (valid / berguna) (misalnya dari konstruktor yang menetapkan semua bidang yang diperlukan, atau metode pabrik yang menetapkan nilai default), dan metode pembangun tambahan kemudian mengembalikan objek yang dimodifikasi berdasarkan pada yang sudah ada. Metode pembangun itu perlu memastikan Anda tidak mendapatkan objek yang tidak valid / tidak konsisten di jalan.

Tentu saja, ini berarti Anda akan memiliki banyak objek baru, dan Anda tidak boleh melakukan ini jika objek Anda mahal untuk dibuat.

Saya menggunakannya dalam kode uji untuk membuat pencocokan Hamcrest untuk salah satu objek bisnis saya. Saya tidak ingat kode persisnya, tetapi terlihat seperti ini (disederhanakan):

public class CustomerMatcher extends TypeSafeMatcher<Customer> {
    private final Matcher<? super String> nameMatcher;
    private final Matcher<? super LocalDate> birthdayMatcher;

    @Override
    protected boolean matchesSafely(Customer c) {
        return nameMatcher.matches(c.getName()) &&
               birthdayMatcher.matches(c.getBirthday());
    }

    private CustomerMatcher(Matcher<? super String> nameMatcher,
                            Matcher<? super LocalDate> birthdayMatcher) {
        this.nameMatcher = nameMatcher;
        this.birthdayMatcher = birthdayMatcher;
    }

    // builder methods from here on

    public static CustomerMatcher isCustomer() {
        // I could return a static instance here instead
        return new CustomerMatcher(Matchers.anything(), Matchers.anything());
    }

    public CustomerMatcher withBirthday(Matcher<? super LocalDate> birthdayMatcher) {
        return new CustomerMatcher(this.nameMatcher, birthdayMatcher);
    }

    public CustomerMatcher withName(Matcher<? super String> nameMatcher) {
        return new CustomerMatcher(nameMatcher, this.birthdayMatcher);
    }
}

Saya kemudian akan menggunakannya seperti ini dalam pengujian unit saya (dengan impor statis yang sesuai):

assertThat(result, is(customer().withName(startsWith("Paŭlo"))));

1
Ini benar - dan saya pikir ini adalah poin yang sangat penting, tetapi tidak menyelesaikan masalah terakhir - Ini tidak memberi Anda kesempatan untuk memvalidasi bahwa objek dalam keadaan konsisten ketika dibangun. Setelah dibangun Anda akan memerlukan beberapa tipu daya seperti memanggil validator sebelum metode bisnis Anda untuk memastikan pemanggil mengatur semua metode (yang akan menjadi praktik yang mengerikan). Saya pikir itu baik untuk bernalar melalui hal semacam ini untuk melihat mengapa kita menggunakan pola Builder seperti yang kita lakukan.
Bill K

@BillK true - objek dengan, pada dasarnya, setter tetap (yang mengembalikan objek baru setiap kali) berarti bahwa setiap objek yang dikembalikan harus valid. Jika Anda mengembalikan objek yang tidak valid (mis. Orang dengan nametetapi tidak age), maka konsep tersebut tidak berfungsi. Yah, Anda sebenarnya bisa mengembalikan sesuatu seperti PartiallyBuiltPersonitu sementara itu tidak valid tetapi sepertinya hack untuk menutupi Builder.
VLAZ

@ BillK inilah yang saya maksud dengan "jika ada cara untuk mendapatkan objek awal (valid / berguna)" - Saya berasumsi bahwa objek awal dan objek yang dikembalikan dari setiap fungsi pembangun adalah valid / konsisten. Tugas Anda sebagai pencipta kelas semacam itu adalah untuk menyediakan hanya metode pembangun yang melakukan perubahan yang konsisten.
Paŭlo Ebermann
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.