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)
quelle
this
?.engine(e)
zweimal für einen Builder aufzurufen ?build()
wenn Sie nicht genannt habenengine(e)
undtransmission(t)
vor.Engine
und diese später mit einer spezifischeren Implementierung überschreiben. Aber höchstwahrscheinlich wäre dies sinnvoller, wennengine(e)
nicht ein Setter, sondern ein Addierer wäre :addEngine(e)
. Dies wäre nützlich für einenCar
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.Antworten:
Sie scheinen zwei unterschiedliche Anforderungen zu haben, basierend auf den von Ihnen angegebenen Methodenaufrufen.
Ich denke , die erste Frage ist hier , dass Sie nicht wissen , was Sie wollen die Klasse zu tun. Ein Teil davon ist, dass nicht bekannt ist, wie das erstellte Objekt aussehen soll.
Ein Auto kann nur einen Motor und ein Getriebe haben. Sogar Hybridautos haben nur einen Motor (vielleicht einen
GasAndElectricEngine
)Ich werde beide Implementierungen ansprechen:
und
Wenn ein Motor und ein Getriebe benötigt werden, sollten sie sich im Konstruktor befinden.
Wenn Sie nicht wissen, welcher Motor oder welches Getriebe benötigt wird, stellen Sie noch keinen ein. Dies ist ein Zeichen dafür, dass Sie den Builder zu weit oben im Stapel erstellen.
quelle
Warum nicht das Nullobjektmuster verwenden? Befreien Sie sich von diesem Builder. Der eleganteste Code, den Sie schreiben können, ist der, den Sie eigentlich nicht schreiben müssen.
quelle
Car
wäre dies sinnvoll, da die Anzahl der c'tor-Argumente sehr gering ist. Sobald Sie sich jedoch mit etwas mäßig Komplexem befassen (> = 4 obligatorische Argumente), wird das Ganze schwieriger zu handhaben / weniger lesbar ("Hat der Motor oder das Getriebe an erster Stelle gestanden?"). Aus diesem Grund würden Sie einen Builder verwenden: Die API zwingt Sie dazu, genauer zu bestimmen, was Sie erstellen.Erstens, wenn Sie nicht viel mehr Zeit haben als in einem Geschäft, in dem ich gearbeitet habe, lohnt es sich wahrscheinlich nicht, eine Reihenfolge der Operationen zuzulassen oder nur damit zu leben, dass Sie mehr als ein Radio angeben können. Beachten Sie, dass es sich um Code handelt, nicht um Benutzereingaben. Daher können Sie Zusicherungen haben, die während des Komponententests und nicht eine Sekunde zuvor beim Kompilieren fehlschlagen.
Wenn Ihre Einschränkung jedoch darin besteht, dass Sie, wie in den Kommentaren angegeben, einen Motor und ein Getriebe benötigen, erzwingen Sie dies, indem Sie alle obligatorischen Eigenschaften als Konstruktor des Erstellers angeben.
Wenn nur die Stereoanlage optional ist, ist der letzte Schritt mit Unterklassen von Buildern möglich, aber darüber hinaus ist der Gewinn durch das Erhalten des Fehlers zur Kompilierungszeit und nicht beim Testen wahrscheinlich nicht die Mühe wert.
quelle
Sie haben bereits die richtige Richtung für diese Frage erraten.
Wenn Sie eine Überprüfung zur Kompilierungszeit erhalten möchten, benötigen Sie
(2^n)
Typen. Wenn Sie eine Laufzeitprüfung erhalten möchten, benötigen Sie eine Variable, in der Status gespeichert(2^n)
werden können. Einen
-bit-Ganzzahl reicht aus.Da C ++ unterstützt nicht-Typ Template - Parameter (zB ganzzahlige Werte) ist es möglich , für eine C ++ Klasse Vorlage in instanziiert werden
O(2^n)
verschiedene Arten, ein Schema ähnlich dem unter Verwendung dieser .In Sprachen, die keine Nicht-Typ-Vorlagenparameter unterstützen, können Sie sich jedoch nicht auf das Typsystem verlassen, um
O(2^n)
verschiedene Typen zu instanziieren .Die nächste Möglichkeit sind Java-Annotationen (und C # -Attribute). Diese zusätzlichen Metadaten können verwendet werden, um beim Kompilieren ein benutzerdefiniertes Verhalten auszulösen, wenn Anmerkungsprozessoren verwendet werden. Es wäre jedoch zu viel Arbeit für Sie, diese zu implementieren. Wenn Sie Frameworks verwenden, die diese Funktionalität für Sie bereitstellen, verwenden Sie sie. Überprüfen Sie andernfalls die nächste Gelegenheit.
Beachten Sie schließlich, dass das Speichern
O(2^n)
verschiedener Zustände als Variable zur Laufzeit (wörtlich als Ganzzahl mit mindestens einern
Breite von Bits) sehr einfach ist. Aus diesem Grund empfehlen die am besten bewerteten Antworten, diese Prüfung zur Laufzeit durchzuführen, da der Aufwand für die Implementierung der Prüfung zur Kompilierungszeit im Vergleich zum potenziellen Gewinn zu hoch ist.quelle