Редагувати: Я хотів би зазначити, що це питання описує теоретичну проблему, і я знаю, що я можу використовувати аргументи конструктора для обов’язкових параметрів або викинути виняток з виконання, якщо API використовується неправильно. Однак я шукаю рішення, яке не вимагає конструкторських аргументів або перевірки часу виконання.
Уявіть, у вас такий Car
інтерфейс:
public interface Car {
public Engine getEngine(); // required
public Transmission getTransmission(); // required
public Stereo getStereo(); // optional
}
Як сказано в коментарі хотіли б запропонувати, Car
повинна мати Engine
і Transmission
але НЕ Stereo
є обов'язковим. Це означає, що Builder, який може build()
мати Car
примірник, повинен коли-небудь мати build()
метод лише тоді, коли Engine
а та Transmission
обидва вже були надані екземпляру будівельника. Таким чином перевірка типу відмовиться компілювати будь-який код, який намагається створити Car
екземпляр без Engine
або Transmission
.
Це вимагає побудови кроків . Зазвичай ви реалізуєте щось подібне:
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;
}
};
}
}
}
Там в ланцюзі різних класів будівельника ( Builder
, BuilderWithEngine
, CompleteBuilder
), що додати один потрібно сетер методу після того, як інший, з останнім класом , що містить всі додаткові методами сетера , як добре.
Це означає, що користувачі цього розробника кроків обмежуються порядком, у якому автор надав обов'язкові сетери . Ось приклад можливих застосувань (зауважте, що всі вони суворо впорядковані: engine(e)
спочатку, після них transmission(t)
, і нарешті, необов’язкові 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();
Однак існує маса сценаріїв, в яких це не ідеально для користувача будівельника, особливо якщо в програмі не тільки сетери, але й добавники, або якщо користувач не може контролювати порядок, у якому певні властивості для будівельника стануть доступними.
Єдине рішення, про яке я можу придумати, є дуже суперечливим: для кожної комбінації обов'язкових властивостей, встановлених або ще не встановлених, я створив спеціальний клас будівельника, який знає, які потенційні інші обов'язкові сетери потрібно викликати перед тим, як прийти вкажіть, де build()
метод повинен бути доступний, і кожен із цих сеттерів повертає більш повний тип конструктора, який знаходиться на крок ближче до вмісту build()
методу.
Я додав код нижче, але ви можете сказати , що я використовую систему типів для створення ФШМ , що дозволяє створити Builder
, який може бути перетворений в яких BuilderWithEngine
або BuilderWithTransmission
, що і тоді , може бути перетворений в CompleteBuilder
, який реалізуєbuild()
метод. Необов’язкові налаштування можуть бути викликані в будь-якому з цих екземплярів програми.
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;
}
};
}
}
}
Як ви можете сказати, це не масштабується, оскільки кількість необхідних різних будівельних класів буде O (2 ^ n), де n - кількість обов'язкових установників.
Звідси моє запитання: чи можна це зробити більш елегантно?
(Я шукаю відповідь, яка працює з Java, хоча Scala також була б прийнятною)
.engine(e)
двічі викликати одного будівельника?
build()
якщо ви не дзвонили engine(e)
і transmission(t)
раніше.
Engine
реалізації, а пізніше замінити її більш конкретною реалізацією. Але , швидше за все , це буде мати більше сенсу , якщо б engine(e)
ні сетер, а акумулятор: addEngine(e)
. Це було б корисно для Car
будівельника, який може виробляти гібридні машини з більш ніж одним двигуном / мотором. Оскільки це надуманий приклад, я не вдавався в деталі щодо того, чому ви можете це зробити - для стислості.
this
?