Bearbeiten: Ich möchte darauf hinweisen, dass diese Frage ein theoretisches Problem beschreibt, und mir ist bewusst, dass ich Konstruktorargumente für obligatorische Parameter verwenden oder eine Laufzeitausnahme auslösen kann, wenn die API falsch verwendet wird. Ich suche jedoch nach einer Lösung, die keine Konstruktorargumente oder Laufzeitprüfungen erfordert.
Stellen Sie sich vor, Sie haben eine Car
Schnittstelle wie diese:
public interface Car {
public Engine getEngine(); // required
public Transmission getTransmission(); // required
public Stereo getStereo(); // optional
}
Wie aus den Kommentaren hervorgeht, ist ein Car
Muss ein Engine
und Transmission
aber ein Stereo
ist optional. Das bedeutet, dass ein Builder, der build()
eine Car
Instanz kann, immer nur eine build()
Methode haben sollte, wenn der Builder-Instanz bereits ein Engine
und Transmission
beide zugewiesen wurden. Auf diese Weise weigert sich die Typprüfung, Code zu kompilieren, der versucht, eine Car
Instanz ohne Engine
oder zu erstellen Transmission
.
Dies erfordert einen Step Builder . Normalerweise implementieren Sie Folgendes:
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;
}
};
}
}
}
Es gibt eine Kette von verschiedenen Builder - Klassen ( Builder
, BuilderWithEngine
, CompleteBuilder
), das Hinzufügen einer erforderlichen Setter - Methode nach der anderen, mit der letzten Klasse alle optionalen Setter - Methoden als auch enthalten.
Dies bedeutet, dass Benutzer dieses Step Builders auf die Reihenfolge beschränkt sind , in der der Autor obligatorische Setter zur Verfügung gestellt hat . Hier ist ein Beispiel für mögliche Verwendungen (beachten Sie, dass alle streng geordnet sind: engine(e)
zuerst, gefolgt von transmission(t)
und schließlich optional 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();
Es gibt jedoch viele Szenarien, in denen dies für den Benutzer des Builders nicht ideal ist, insbesondere wenn der Builder nicht nur Setter, sondern auch Addierer hat oder wenn der Benutzer die Reihenfolge nicht steuern kann, in der bestimmte Eigenschaften für den Builder verfügbar werden.
Die einzige Lösung, die ich mir vorstellen kann , ist sehr kompliziert: Für jede Kombination von obligatorischen Eigenschaften, die festgelegt wurden oder noch nicht festgelegt wurden, habe ich eine dedizierte Builder-Klasse erstellt, die weiß, welche potenziellen anderen obligatorischen Setter aufgerufen werden müssen, bevor sie zu a gelangen Geben Sie an, wo die build()
Methode verfügbar sein soll, und jeder dieser Setter gibt einen vollständigeren Builder-Typ zurück, der dem Enthalten einer build()
Methode einen Schritt näher kommt .
Ich habe den folgenden Code hinzugefügt, aber Sie könnten sagen, dass ich das Typsystem verwende, um einen FSM zu erstellen, mit dem Sie einen erstellen Builder
können, der entweder in einen BuilderWithEngine
oder BuilderWithTransmission
in einen verwandelt werden kann, und beide können dann in einen umgewandelt werden CompleteBuilder
, der den implementiertbuild()
Methode. Optionale Setter können für jede dieser Builder-Instanzen aufgerufen werden.
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;
}
};
}
}
}
Wie Sie sehen können, lässt sich dies nicht gut skalieren, da die Anzahl der verschiedenen erforderlichen Builder-Klassen O (2 ^ n) wäre, wobei n die Anzahl der obligatorischen Setter ist.
Daher meine Frage: Kann das eleganter gemacht werden?
(Ich suche nach einer Antwort, die mit Java funktioniert, obwohl Scala auch akzeptabel wäre)
.engine(e)
zweimal für einen Builder aufzurufen ?
build()
wenn Sie nicht genannt haben engine(e)
und transmission(t)
vor.
Engine
und diese später mit einer spezifischeren Implementierung überschreiben. Aber höchstwahrscheinlich wäre dies sinnvoller, wenn engine(e)
nicht ein Setter, sondern ein Addierer wäre : addEngine(e)
. Dies wäre nützlich für einen Car
Hersteller, der Hybridautos mit mehr als einem Motor herstellen kann. Da dies ein erfundenes Beispiel ist, habe ich nicht näher darauf eingegangen, warum Sie dies tun möchten - der Kürze halber.
this
?