Изменить: я хотел бы отметить, что этот вопрос описывает теоретическую проблему, и я знаю, что я могу использовать аргументы конструктора для обязательных параметров, или вызвать исключение времени выполнения, если 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?