Pola Pembangun: Kapan harus gagal?

45

Ketika menerapkan Pola Pembangun, saya sering merasa bingung dengan kapan membiarkan bangunan gagal dan saya bahkan berhasil mengambil pendirian yang berbeda tentang masalah ini setiap beberapa hari.

Pertama, beberapa penjelasan:

  • Dengan gagal awal saya maksudkan bahwa membangun objek harus gagal segera setelah parameter yang tidak valid diteruskan. Jadi di dalam SomeObjectBuilder.
  • Dengan gagal terlambat saya maksudkan bahwa membangun objek hanya bisa gagal pada build()panggilan yang secara implisit memanggil konstruktor objek yang akan dibangun.

Lalu beberapa argumen:

  • Dalam mendukung kegagalan terlambat: Kelas pembangun harus tidak lebih dari kelas yang hanya memegang nilai-nilai. Selain itu, ini menyebabkan duplikasi kode lebih sedikit.
  • Dalam mendukung kegagalan awal: Pendekatan umum dalam pemrograman perangkat lunak adalah bahwa Anda ingin mendeteksi masalah sedini mungkin dan oleh karena itu tempat yang paling logis untuk memeriksa akan berada di 'pembangun' konstruktor, 'setter' dan akhirnya dalam metode membangun.

Apa konsensus umum tentang ini?

skiwi
sumber
8
Saya tidak melihat ada untungnya gagal terlambat. Apa yang seseorang katakan sebagai kelas pembangun "harus" tidak diutamakan daripada desain yang baik, dan menangkap bug lebih awal selalu lebih baik daripada terlambat menangkap bug.
Doval
3
Cara lain untuk melihat ini adalah bahwa pembangun mungkin tidak tahu apa data yang valid. Gagal lebih awal dalam kasus ini adalah tentang gagal segera setelah Anda tahu ada kesalahan. Tidak gagal awal, akan menjadi pembangun mengembalikan nullobjek ketika ada masalah di build().
Chris
Jika Anda tidak menambahkan cara untuk mengeluarkan peringatan dan menawarkan cara untuk memperbaiki dalam pembangun tidak ada gunanya terlambat.
Markus

Jawaban:

34

Mari kita lihat opsi, di mana kita dapat menempatkan kode validasi:

  1. Di dalam setter dalam pembangun.
  2. Di dalam build()metode.
  3. Di dalam entitas yang dibangun: itu akan dipanggil dalam build()metode ketika entitas sedang dibuat.

Opsi 1 memungkinkan kita untuk mendeteksi masalah sebelumnya, tetapi ada kasus rumit ketika kita dapat memvalidasi input hanya memiliki konteks penuh, dengan demikian, melakukan setidaknya sebagian dari validasi dalam build()metode. Dengan demikian, memilih opsi 1 akan menyebabkan kode tidak konsisten dengan bagian validasi dilakukan di satu tempat dan bagian lain dilakukan di tempat lain.

Opsi 2 tidak jauh lebih buruk daripada opsi 1, karena, biasanya, seter dalam builder dipanggil tepat sebelum build(), terutama, pada antarmuka yang lancar. Dengan demikian, masih mungkin untuk mendeteksi masalah cukup awal dalam kebanyakan kasus. Namun, jika pembuat bukan satu-satunya cara untuk membuat objek, itu akan mengarah pada duplikasi kode validasi, karena Anda harus memilikinya di mana pun Anda membuat objek. Solusi paling logis dalam hal ini adalah menempatkan validasi sedekat mungkin ke objek yang dibuat, yaitu di dalamnya. Dan ini adalah opsi 3 .

Dari sudut pandang SOLID, menempatkan validasi dalam builder juga melanggar SRP: kelas builder sudah memiliki tanggung jawab untuk menggabungkan data untuk membangun objek. Validasi adalah membuat kontrak dengan keadaan internal sendiri, itu adalah tanggung jawab baru untuk memeriksa keadaan objek lain.

Jadi, dari sudut pandang saya, tidak hanya lebih baik gagal terlambat dari perspektif desain, tetapi juga lebih baik gagal di dalam entitas yang dibangun, daripada di builder itu sendiri.

UPD: komentar ini mengingatkan saya pada satu kemungkinan lagi, ketika validasi di dalam builder (opsi 1 atau 2) masuk akal. Masuk akal jika pembangun memiliki kontrak sendiri pada objek yang dibuatnya. Sebagai contoh, asumsikan bahwa kita memiliki pembangun yang membangun string dengan konten spesifik, katakanlah, daftar rentang angka 1-2,3-4,5-6. Pembangun ini mungkin memiliki metode seperti addRange(int min, int max). String yang dihasilkan tidak tahu apa-apa tentang angka-angka ini, tidak juga harus tahu. Pembangun itu sendiri mendefinisikan format string dan batasan pada angka. Dengan demikian, metode addRange(int,int)harus memvalidasi angka input dan melemparkan pengecualian jika maks kurang dari min.

Yang mengatakan, aturan umum akan memvalidasi hanya kontrak yang ditentukan oleh pembangun itu sendiri.

Ivan Gammel
sumber
Saya pikir perlu dicatat bahwa sementara Opsi 1 dapat menyebabkan waktu pemeriksaan yang "tidak konsisten", opsi itu masih dapat dipandang konsisten jika semuanya "sedini mungkin." Ini sedikit lebih mudah untuk membuat "sedini mungkin" lebih pasti jika varian builder, StepBuilder, digunakan sebagai gantinya.
Joshua Taylor
Jika pembangun URI melempar pengecualian jika string nol dilewatkan, apakah ini merupakan pelanggaran SOLID? Sampah
Gusdor
@ Goddor ya, jika itu melempar pengecualian itu sendiri. Namun, dari sudut pandang pengguna, semua opsi yang tampak seperti pengecualian dilemparkan oleh pembangun.
Ivan Gammel
Jadi mengapa tidak memiliki validasi () yang dipanggil oleh build ()? Dengan begitu ada sedikit duplikasi, konsistensi dan tidak ada pelanggaran SRP. Itu juga memungkinkan untuk memvalidasi data tanpa berusaha membangun, dan validasinya dekat dengan pembuatan.
StellarVortex
@StellarVortex dalam hal ini akan divalidasi dua kali - satu kali di builder.build (), dan, jika datanya valid dan kami melanjutkan ke konstruktor objek, dalam konstruktor itu.
Ivan Gammel
34

Mengingat Anda menggunakan Java, pertimbangkan panduan otoritatif dan terperinci yang disediakan oleh Joshua Bloch dalam artikel Membuat dan Menghancurkan Objek Java (huruf tebal dalam kutipan di bawah ini adalah milik saya):

Seperti konstruktor, pembangun dapat memaksakan invarian pada parameternya. Metode build dapat memeriksa invarian ini. Sangat penting bahwa mereka diperiksa setelah menyalin parameter dari pembangun ke objek, dan bahwa mereka diperiksa pada bidang objek daripada bidang pembangun (Item 39). Jika ada invarian yang dilanggar, metode build harus membuang IllegalStateException(Item 60). Metode detail pengecualian harus menunjukkan invarian mana yang dilanggar (Butir 63).

Cara lain untuk memaksakan invarian yang melibatkan banyak parameter adalah dengan memiliki metode setter mengambil seluruh grup parameter yang harus dipegang oleh beberapa invarian. Jika invarian tidak puas, metode setter melempar IllegalArgumentException. Ini memiliki keuntungan mendeteksi kegagalan invarian segera setelah parameter yang tidak valid diteruskan, alih-alih menunggu build dipanggil.

Catatan menurut penjelasan editor pada artikel ini, "item" dalam kutipan di atas merujuk pada aturan yang disajikan dalam Java Efektif, Edisi Kedua .

Artikel ini tidak menjelaskan mengapa ini direkomendasikan, tetapi jika Anda memikirkannya, alasannya cukup jelas. Tip umum tentang pemahaman ini disediakan di sana dalam artikel, dalam penjelasan bagaimana konsep pembangun terhubung dengan konstruktor - dan kelas invarian diharapkan diperiksa dalam konstruktor, bukan dalam kode lain yang dapat mendahului / menyiapkan permintaannya.

Untuk pemahaman yang lebih konkret tentang mengapa memeriksa invarian sebelum memohon build akan salah, pertimbangkan contoh populer CarBuilder . Metode pembangun dapat dipanggil dalam urutan sewenang-wenang dan sebagai hasilnya, orang tidak dapat benar-benar tahu apakah parameter tertentu valid hingga bangunan.

Pertimbangkan bahwa mobil sport tidak dapat memiliki lebih dari 2 kursi, bagaimana orang bisa tahu apakah setSeats(4)boleh atau tidak? Itu hanya di build ketika seseorang bisa tahu pasti apakah setSportsCar()dipanggil atau tidak, yang berarti apakah akan melempar TooManySeatsExceptionatau tidak.

agas
sumber
3
+1 untuk merekomendasikan jenis pengecualian yang akan dilempar, persis apa yang saya cari.
Xantix
Tidak yakin saya mendapatkan alternatifnya. Tampaknya menjadi pembicaraan murni ketika invarian hanya dapat divalidasi dalam kelompok. Pembangun menerima atribut tunggal ketika mereka tidak melibatkan yang lain, dan hanya menerima grup atribut ketika grup memiliki invarian pada dirinya sendiri. Dalam hal ini, haruskah atribut tunggal melemparkan pengecualian sebelum build?
Didier A.
19

Nilai tidak valid yang tidak valid karena tidak dapat ditoleransi harus segera diketahui menurut pendapat saya. Dengan kata lain, jika Anda hanya menerima angka positif, dan angka negatif dilewatkan, tidak perlu harus menunggu sampai build()dipanggil. Saya tidak akan mempertimbangkan ini jenis masalah yang Anda "harapkan" telah terjadi, karena itu merupakan prasyarat untuk memanggil metode untuk memulai. Dengan kata lain, Anda tidak akan bergantung pada kegagalan pengaturan parameter tertentu. Kemungkinan besar Anda akan menganggap parameternya benar atau Anda akan melakukan beberapa pemeriksaan sendiri.

Namun, untuk masalah yang lebih rumit yang tidak mudah divalidasi mungkin lebih baik diketahui saat Anda menelepon build(). Contoh yang baik dari ini mungkin menggunakan informasi koneksi yang Anda berikan untuk membuat koneksi ke database. Dalam hal ini, sementara Anda secara teknis dapat memeriksa kondisi seperti itu, itu tidak lagi intuitif dan hanya mempersulit kode Anda. Seperti yang saya lihat, ini juga merupakan jenis masalah yang mungkin benar-benar terjadi dan Anda tidak dapat benar-benar mengantisipasi sampai Anda mencobanya. Ini semacam perbedaan antara mencocokkan string dengan ekspresi reguler untuk melihat apakah itu dapat diuraikan sebagai int dan hanya mencoba menguraikannya, menangani setiap pengecualian potensial yang mungkin terjadi sebagai konsekuensi.

Saya biasanya tidak suka melempar pengecualian ketika mengatur parameter karena itu berarti harus menangkap pengecualian yang dilemparkan, jadi saya cenderung mendukung validasi dalam build(). Jadi untuk alasan ini, saya lebih suka menggunakan RuntimeException karena sekali lagi, kesalahan dalam parameter yang dikirimkan seharusnya tidak terjadi secara umum.

Namun, ini lebih merupakan praktik terbaik daripada apa pun. Saya harap itu menjawab pertanyaan Anda.

Neil
sumber
11

Sejauh yang saya tahu, praktik umum (tidak yakin apakah ada konsensus) adalah gagal sedini mungkin Anda dapat menemukan kesalahan. Ini juga membuat lebih sulit untuk menyalahgunakan API Anda secara tidak sengaja.

Jika itu adalah atribut sepele yang dapat diperiksa pada input, seperti kapasitas atau panjang yang seharusnya tidak negatif, maka Anda sebaiknya segera gagal. Menahan kesalahan meningkatkan jarak antara kesalahan dan umpan balik, yang membuatnya lebih sulit untuk menemukan sumber masalah.

Jika Anda tidak beruntung berada dalam situasi di mana validitas atribut tergantung pada orang lain, maka Anda memiliki dua pilihan:

  • Mengharuskan kedua (atau lebih) atribut diberikan secara bersamaan (yaitu pemanggilan metode tunggal).
  • Uji validitas segera setelah Anda tahu tidak ada lagi perubahan yang masuk: kapan build()atau lebih disebut.

Seperti kebanyakan hal, ini adalah keputusan yang dibuat dalam konteks. Jika konteksnya membuat canggung atau rumit untuk gagal lebih awal, trade-off dapat dilakukan untuk menunda pemeriksaan di lain waktu, tetapi gagal-cepat harus menjadi default.

JvR
sumber
Jadi untuk meringkas, Anda mengatakan bahwa masuk akal untuk memvalidasi sedini mungkin segala sesuatu yang mungkin telah tercakup dalam objek / tipe primitif? Seperti unsigned,, @NonNulldll.
skiwi
2
@skiwi Cukup banyak, ya. Cek domain, cek nol, hal semacam itu. Saya tidak akan menganjurkan menempatkan lebih banyak di dalamnya daripada itu: pembangun umumnya hal-hal sederhana.
JvR
1
Mungkin perlu dicatat bahwa jika validitas satu parameter tergantung pada nilai yang lain, satu hanya dapat secara sah menolak nilai parameter jika satu tahu bahwa yang lain "benar-benar" didirikan . Jika diizinkan untuk menetapkan nilai parameter beberapa kali [dengan pengaturan terakhir didahulukan], maka dalam beberapa kasus cara paling alami untuk mengatur objek adalah dengan mengatur parameter Xke nilai yang tidak valid mengingat nilai sekarang Y, tetapi sebelum menelepon build()diatur Yke nilai yang akan membuat Xvalid.
supercat
Jika misalnya seseorang sedang membangun Shapedan pembangun memiliki WithLeftdan WithRightproperti, dan seseorang ingin menyesuaikan pembangun untuk membangun objek di tempat yang berbeda, memerlukan yang WithRightdisebut pertama ketika memindahkan objek ke kanan, dan WithLeftketika memindahkannya ke kiri, akan menambah kompleksitas yang tidak perlu dibandingkan dengan memungkinkan WithLeftuntuk mengatur tepi kiri ke kanan dari tepi kanan lama asalkan WithRightmemperbaiki tepi kanan sebelumnya builddisebut.
supercat
0

Aturan dasarnya adalah "gagal lebih awal".

Aturan yang sedikit lebih maju adalah "gagal sedini mungkin".

Jika sebuah properti pada dasarnya tidak valid ...

CarBuilder.numberOfWheels( -1 ). ...  

... maka Anda langsung menolaknya.

Kasing lain mungkin perlu nilai diperiksa dalam kombinasi dan mungkin lebih baik ditempatkan dalam metode build ():

CarBuilder.numberOfWheels( 0 ).type( 'Hovercraft' ). ...  
Phill W.
sumber