Sunting: Saya ingin menunjukkan bahwa pertanyaan ini menjelaskan masalah teoritis, dan saya sadar bahwa saya dapat menggunakan argumen konstruktor untuk parameter wajib, atau melempar pengecualian runtime jika API digunakan secara tidak benar. Namun, saya mencari solusi yang tidak memerlukan argumen konstruktor atau pengecekan runtime.
Bayangkan Anda memiliki Car
antarmuka seperti ini:
public interface Car {
public Engine getEngine(); // required
public Transmission getTransmission(); // required
public Stereo getStereo(); // optional
}
Seperti yang disarankan oleh komentar, a Car
harus memiliki Engine
dan Transmission
tetapi a Stereo
adalah opsional. Bahwa sarana Builder yang dapat build()
menjadi Car
contoh hanya pernah memiliki build()
metode jika Engine
dan Transmission
memiliki keduanya telah diberikan ke contoh pembangun. Dengan cara itu pemeriksa tipe akan menolak untuk mengkompilasi kode apa pun yang mencoba membuat Car
instance tanpa Engine
atau Transmission
.
Ini panggilan untuk Pembangun Langkah . Biasanya Anda akan menerapkan sesuatu seperti ini:
public interface Car {
public Engine getEngine(); // required
public Transmission getTransmission(); // required
public Stereo getStereo(); // optional
public class Builder {
public BuilderWithEngine engine(Engine engine) {
return new BuilderWithEngine(engine);
}
}
public class BuilderWithEngine {
private Engine engine;
private BuilderWithEngine(Engine engine) {
this.engine = engine;
}
public BuilderWithEngine engine(Engine engine) {
this.engine = engine;
return this;
}
public CompleteBuilder transmission(Transmission transmission) {
return new CompleteBuilder(engine, transmission);
}
}
public class CompleteBuilder {
private Engine engine;
private Transmission transmission;
private Stereo stereo = null;
private CompleteBuilder(Engine engine, Transmission transmission) {
this.engine = engine;
this.transmission = transmission;
}
public CompleteBuilder engine(Engine engine) {
this.engine = engine;
return this;
}
public CompleteBuilder transmission(Transmission transmission) {
this.transmission = transmission;
return this;
}
public CompleteBuilder stereo(Stereo stereo) {
this.stereo = stereo;
return this;
}
public Car build() {
return new Car() {
@Override
public Engine getEngine() {
return engine;
}
@Override
public Transmission getTransmission() {
return transmission;
}
@Override
public Stereo getStereo() {
return stereo;
}
};
}
}
}
Ada rantai kelas pembangun yang berbeda ( Builder
, BuilderWithEngine
, CompleteBuilder
), bahwa salah satu add diperlukan metode setter demi satu, dengan kelas terakhir yang berisi semua metode setter opsional juga.
Ini berarti bahwa pengguna pembuat langkah ini terbatas pada urutan di mana penulis membuat setter wajib tersedia . Berikut ini adalah contoh kemungkinan penggunaan (perhatikan bahwa semuanya dipesan dengan ketat: engine(e)
pertama, diikuti oleh transmission(t)
, dan akhirnya opsional stereo(s)
).
new Builder().engine(e).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).engine(e).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).engine(e).build();
new Builder().engine(e).transmission(t).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).stereo(s).build();
Namun, ada banyak skenario di mana ini tidak ideal untuk pengguna pembangun, terutama jika pembangun tidak hanya seter, tetapi juga adders, atau jika pengguna tidak dapat mengontrol urutan di mana properti tertentu untuk pembangun akan tersedia.
Satu-satunya solusi yang dapat saya pikirkan untuk itu sangat berbelit-belit: Untuk setiap kombinasi properti wajib yang telah ditetapkan atau yang belum ditetapkan, saya membuat kelas pembangun khusus yang tahu potensi mana yang harus ditetapkan oleh pembuat seting wajib lainnya sebelum tiba di sebuah nyatakan di mana build()
metode harus tersedia, dan masing-masing setter mengembalikan tipe pembangun yang lebih lengkap yang selangkah lebih dekat untuk memuat build()
metode.
Saya menambahkan kode di bawah ini, tetapi Anda mungkin mengatakan bahwa saya menggunakan sistem tipe untuk membuat FSM yang memungkinkan Anda membuat Builder
, yang dapat diubah menjadi a BuilderWithEngine
atau BuilderWithTransmission
, yang keduanya kemudian dapat diubah menjadi CompleteBuilder
, yang mengimplementasikanbuild()
metode. Setter opsional dapat digunakan pada instance builder ini.
public interface Car {
public Engine getEngine(); // required
public Transmission getTransmission(); // required
public Stereo getStereo(); // optional
public class Builder extends OptionalBuilder {
public BuilderWithEngine engine(Engine engine) {
return new BuilderWithEngine(engine, stereo);
}
public BuilderWithTransmission transmission(Transmission transmission) {
return new BuilderWithTransmission(transmission, stereo);
}
@Override
public Builder stereo(Stereo stereo) {
super.stereo(stereo);
return this;
}
}
public class OptionalBuilder {
protected Stereo stereo = null;
private OptionalBuilder() {}
public OptionalBuilder stereo(Stereo stereo) {
this.stereo = stereo;
return this;
}
}
public class BuilderWithEngine extends OptionalBuilder {
private Engine engine;
private BuilderWithEngine(Engine engine, Stereo stereo) {
this.engine = engine;
this.stereo = stereo;
}
public CompleteBuilder transmission(Transmission transmission) {
return new CompleteBuilder(engine, transmission, stereo);
}
public BuilderWithEngine engine(Engine engine) {
this.engine = engine;
return this;
}
@Override
public BuilderWithEngine stereo(Stereo stereo) {
super.stereo(stereo);
return this;
}
}
public class BuilderWithTransmission extends OptionalBuilder {
private Transmission transmission;
private BuilderWithTransmission(Transmission transmission, Stereo stereo) {
this.transmission = transmission;
this.stereo = stereo;
}
public CompleteBuilder engine(Engine engine) {
return new CompleteBuilder(engine, transmission, stereo);
}
public BuilderWithTransmission transmission(Transmission transmission) {
this.transmission = transmission;
return this;
}
@Override
public BuilderWithTransmission stereo(Stereo stereo) {
super.stereo(stereo);
return this;
}
}
public class CompleteBuilder extends OptionalBuilder {
private Engine engine;
private Transmission transmission;
private CompleteBuilder(Engine engine, Transmission transmission, Stereo stereo) {
this.engine = engine;
this.transmission = transmission;
this.stereo = stereo;
}
public CompleteBuilder engine(Engine engine) {
this.engine = engine;
return this;
}
public CompleteBuilder transmission(Transmission transmission) {
this.transmission = transmission;
return this;
}
@Override
public CompleteBuilder stereo(Stereo stereo) {
super.stereo(stereo);
return this;
}
public Car build() {
return new Car() {
@Override
public Engine getEngine() {
return engine;
}
@Override
public Transmission getTransmission() {
return transmission;
}
@Override
public Stereo getStereo() {
return stereo;
}
};
}
}
}
Seperti yang Anda tahu, ini tidak baik skala, karena jumlah kelas pembangun yang berbeda diperlukan akan O (2 ^ n) di mana n adalah jumlah setter wajib.
Maka pertanyaan saya: Apakah ini bisa dilakukan dengan lebih elegan?
(Saya mencari jawaban yang berfungsi dengan Java, meskipun Scala juga dapat diterima)
sumber
this
?.engine(e)
dua kali untuk satu pembangun?build()
jika Anda belum meneleponengine(e)
dantransmission(t)
sebelumnya.Engine
implementasi default , dan kemudian menimpanya dengan implementasi yang lebih spesifik. Tapi kemungkinan besar ini akan lebih masuk akal jikaengine(e)
tidak setter, tetapi penambah:addEngine(e)
. Ini akan berguna untukCar
pembangun yang dapat menghasilkan mobil hybrid dengan lebih dari satu mesin / motor. Karena ini adalah contoh yang dibuat-buat, saya tidak masuk ke detail tentang mengapa Anda mungkin ingin melakukan ini - untuk singkatnya.Jawaban:
Anda tampaknya memiliki dua persyaratan berbeda, berdasarkan metode panggilan yang Anda berikan.
Saya pikir masalah pertama di sini adalah Anda tidak tahu apa yang Anda inginkan dari kelas. Sebagian dari hal itu adalah tidak diketahui seperti apa objek yang Anda inginkan.
Mobil hanya dapat memiliki satu mesin dan satu transmisi. Bahkan mobil hybrid hanya memiliki satu mesin (mungkin a
GasAndElectricEngine
)Saya akan membahas kedua implementasi:
dan
Jika mesin dan transmisi diperlukan, maka mereka harus berada di konstruktor.
Jika Anda tidak tahu mesin atau transmisi apa yang dibutuhkan, maka jangan menyetelnya; itu pertanda bahwa Anda membuat pembuat terlalu jauh dari tumpukan.
sumber
Mengapa tidak menggunakan pola objek nol? Singkirkan pembangun ini, kode paling elegan yang bisa Anda tulis adalah kode yang sebenarnya tidak perlu Anda tulis.
sumber
Car
, ini masuk akal karena jumlah argumen c'tor sangat kecil. Namun, segera setelah Anda berurusan dengan sesuatu yang cukup kompleks (> = 4 argumen wajib), semuanya menjadi lebih sulit untuk ditangani / kurang dapat dibaca ("Apakah mesin atau transmisi didahulukan?"). Itu sebabnya Anda akan menggunakan pembuat: API memaksa Anda untuk lebih eksplisit tentang apa yang Anda buat.Pertama, kecuali jika Anda memiliki lebih banyak waktu daripada toko tempat saya bekerja, mungkin tidak ada gunanya mengizinkan urutan operasi apa pun atau hanya hidup dengan fakta bahwa Anda dapat menentukan lebih dari satu radio. Perhatikan bahwa Anda berbicara tentang kode, bukan input pengguna, sehingga Anda dapat memiliki pernyataan yang akan gagal selama pengujian unit Anda daripada satu detik sebelumnya pada waktu kompilasi.
Namun, jika kendala Anda, seperti yang diberikan dalam komentar, bahwa Anda harus memiliki mesin dan transmisi, maka tegakkan ini dengan meletakkan semua properti yang diwajibkan adalah konstruktor pembangun.
Jika hanya stereo yang opsional, maka melakukan langkah terakhir menggunakan subkelas pembangun adalah mungkin, tetapi di luar itu keuntungan dari mendapatkan kesalahan pada waktu kompilasi daripada dalam pengujian mungkin tidak sepadan dengan usaha.
sumber
Anda sudah menebak arah yang benar untuk pertanyaan ini.
Jika Anda ingin mendapatkan pemeriksaan waktu kompilasi, Anda perlu beberapa
(2^n)
tipe. Jika Anda ingin mendapatkan pemeriksaan run-time, Anda akan memerlukan variabel yang dapat menyimpan(2^n)
status; sebuahn
bilangan bulat-bit akan melakukan.Karena C ++ mendukung parameter templat non-tipe (mis. Nilai integer) , dimungkinkan untuk templat kelas C ++ dipakai di dalam
O(2^n)
tipe yang berbeda, menggunakan skema yang mirip dengan ini .Namun, dalam bahasa yang tidak mendukung parameter template non-tipe, Anda tidak bisa mengandalkan sistem tipe untuk instantiate
O(2^n)
tipe yang berbeda.Peluang berikutnya adalah dengan penjelasan Java (dan atribut C #). Metadata tambahan ini dapat digunakan untuk memicu perilaku yang ditentukan pengguna pada waktu kompilasi, saat prosesor anotasi digunakan. Namun, akan terlalu sulit bagi Anda untuk mengimplementasikannya. Jika Anda menggunakan kerangka kerja yang menyediakan fungsionalitas ini untuk Anda, gunakan itu. Jika tidak, periksa peluang berikutnya.
Akhirnya, perhatikan bahwa menyimpan
O(2^n)
status yang berbeda sebagai variabel saat run-time (secara harfiah, sebagai integer yang memilikin
lebar bit minimal ) sangat mudah. Inilah sebabnya mengapa jawaban yang paling banyak dipilih menyarankan Anda untuk melakukan pemeriksaan ini pada saat run-time, karena upaya yang diperlukan untuk mengimplementasikan pemeriksaan waktu kompilasi terlalu banyak, jika dibandingkan dengan potensi keuntungan.sumber