Изменить: я хотел бы отметить, что этот вопрос описывает теоретическую проблему, и я знаю, что я могу использовать аргументы конструктора для обязательных параметров, или вызвать исключение времени выполнения, если 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
оба уже были переданы экземпляру Builder. Таким образом, средство проверки типов откажется компилировать любой код, который пытается создать Car
экземпляр без Engine
или Transmission
.
Это требует Step Builder . Обычно вы реализуете что-то вроде этого:
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()
метода.
Я добавил приведенный ниже код, но вы можете сказать, что я использую систему типов для создания FSM, который позволяет вам создать a Builder
, который можно превратить в a BuilderWithEngine
или or BuilderWithTransmission
, который затем можно превратить в a 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
?