Java: Bagaimana menerapkan pembangun langkah yang tidak memedulikan urutan setter?

10

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 Carantarmuka seperti ini:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional
}

Seperti yang disarankan oleh komentar, a Carharus memiliki Enginedan Transmissiontetapi a Stereoadalah opsional. Bahwa sarana Builder yang dapat build()menjadi Carcontoh hanya pernah memiliki build()metode jika Enginedan Transmissionmemiliki keduanya telah diberikan ke contoh pembangun. Dengan cara itu pemeriksa tipe akan menolak untuk mengkompilasi kode apa pun yang mencoba membuat Carinstance tanpa Engineatau 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 BuilderWithEngineatau BuilderWithTransmission, yang keduanya kemudian dapat diubah menjadi CompleteBuilder, yang mengimplementasikanbuild()metode. Setter opsional dapat digunakan pada instance builder ini. masukkan deskripsi gambar di sini

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)

derabbink
sumber
1
Apa yang mencegah Anda dari hanya menggunakan wadah IoC untuk berdiri semua dependensi ini? Juga, saya tidak jelas mengapa, jika pesanan tidak masalah seperti yang Anda tegaskan, Anda tidak bisa hanya menggunakan metode setter biasa yang kembali this?
Robert Harvey
Apa artinya memanggil .engine(e)dua kali untuk satu pembangun?
Erik Eidt
3
Jika Anda ingin memverifikasinya secara statis tanpa secara manual menulis kelas untuk setiap kombinasi, Anda mungkin harus menggunakan hal-hal tingkat neckbeard seperti makro atau pemrograman metaplat. Java tidak cukup ekspresif untuk itu, sepengetahuan saya, dan upaya itu mungkin tidak akan sepadan dengan solusi yang diverifikasi secara dinamis dalam bahasa lain.
Karl Bielefeldt
1
Robert: Tujuannya adalah agar pemeriksa tipe menegakkan fakta bahwa baik mesin maupun transmisi adalah wajib; dengan cara ini Anda bahkan tidak dapat menelepon build()jika Anda belum menelepon engine(e)dan transmission(t)sebelumnya.
derabbink
Erik: Anda mungkin ingin memulai dengan Engineimplementasi default , dan kemudian menimpanya dengan implementasi yang lebih spesifik. Tapi kemungkinan besar ini akan lebih masuk akal jika engine(e)tidak setter, tetapi penambah: addEngine(e). Ini akan berguna untuk Carpembangun 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.
derabbink

Jawaban:

3

Anda tampaknya memiliki dua persyaratan berbeda, berdasarkan metode panggilan yang Anda berikan.

  1. Hanya satu mesin (wajib), hanya satu transmisi (wajib), dan hanya satu stereo (opsional).
  2. Satu atau lebih mesin (wajib), satu atau lebih transmisi (wajib), dan satu atau lebih stereo (opsional).

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:

public class CarBuilder {

    public CarBuilder(Engine engine, Transmission transmission) {
        // ...
    }

    public CarBuilder setStereo(Stereo stereo) {
        // ...
        return this;
    }
}

dan

public class CarBuilder {

    public CarBuilder(List<Engine> engines, List<Transmission> transmission) {
        // ...
    }

    public CarBuilder addStereo(Stereo stereo) {
        // ...
        return this;
    }
}

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.

Zymus
sumber
2

Mengapa tidak menggunakan pola objek nol? Singkirkan pembangun ini, kode paling elegan yang bisa Anda tulis adalah kode yang sebenarnya tidak perlu Anda tulis.

public final class CarImpl implements Car {
    private final Engine engine;
    private final Transmission transmission;
    private final Stereo stereo;

    public CarImpl(Engine engine, Transmission transmission) {
        this(engine, transmission, new DefaultStereo());
    }

    public CarImpl(Engine engine, Transmission transmission, Stereo stereo) {
        this.engine = engine;
        this.transmission = transmission;
        this.stereo = stereo;
    }

    //...

}
Tutul
sumber
Inilah yang saya pikirkan segera. walaupun saya tidak akan memiliki konstruktor tiga parameter, hanya konstruktor dua parameter yang memiliki elemen wajib dan kemudian setter untuk stereo karena bersifat opsional.
Encaitar
1
Dalam contoh sederhana (dibuat-buat) seperti 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.
derabbink
1
@ Derabbink Mengapa tidak melanggar kelas Anda dalam yang lebih kecil dalam hal ini? Menggunakan pembangun hanya akan menyembunyikan fakta bahwa kelas melakukan terlalu banyak dan menjadi tidak dapat dipelihara.
Melihat
1
Kudos untuk mengakhiri kegilaan pola.
Robert Harvey
@Terlihat beberapa kelas hanya berisi banyak data. Misalnya, jika Anda ingin menghasilkan kelas log akses yang menampung semua info terkait tentang permintaan HTTP dan output data ke dalam format CSV atau JSON. Akan ada banyak data dan jika Anda ingin menegakkan beberapa bidang yang akan hadir selama waktu kompilasi, Anda akan memerlukan pola pembangun dengan konstruktor daftar arg yang sangat panjang, yang tidak terlihat bagus.
ssgao
1

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.

new Builder(e, t).build();                      // ok
new Builder(e, t).stereo(s).build();            // ok
new Builder(e, t).stereo(s).stereo(s).build();  // exception on second call to stereo as stereo is already set 

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.

Pete Kirkham
sumber
0

jumlah kelas pembangun berbeda yang dibutuhkan adalah O (2 ^ n) dengan n adalah jumlah setter wajib.

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; sebuah nbilangan 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 memiliki nlebar 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.

rwong
sumber