Edit: je voudrais souligner que cette question décrit un problème théorique, et je suis conscient que je peux utiliser des arguments de constructeur pour les paramètres obligatoires, ou lever une exception d'exécution si l'API est utilisée de manière incorrecte. Cependant, je recherche une solution qui ne nécessite pas d'arguments constructeur ni de vérification d'exécution.
Imaginez que vous ayez une Car
interface comme celle-ci:
public interface Car {
public Engine getEngine(); // required
public Transmission getTransmission(); // required
public Stereo getStereo(); // optional
}
Comme le suggèrent les commentaires, un Car
must have Engine
et Transmission
mais a Stereo
est facultatif. Cela signifie qu'un générateur qui peut build()
une Car
instance ne devrait avoir une build()
méthode que si un Engine
et Transmission
ont déjà été donnés à l'instance du générateur. De cette façon, le vérificateur de type refusera de compiler tout code qui tente de créer une Car
instance sans Engine
ou Transmission
.
Cela nécessite un Step Builder . En règle générale, vous implémentez quelque chose comme ceci:
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;
}
};
}
}
}
Il y a une chaîne de différentes classes de constructeur ( Builder
, BuilderWithEngine
, CompleteBuilder
), que l' un d'ajout requis méthode setter après l' autre, avec la dernière classe contenant toutes les méthodes de réglage en option ainsi.
Cela signifie que les utilisateurs de ce générateur d'étapes sont limités à l'ordre dans lequel l'auteur a rendu les paramètres obligatoires disponibles . Voici un exemple d'utilisations possibles (notez qu'elles sont toutes strictement ordonnées: d' engine(e)
abord, suivies transmission(t)
et enfin facultatives 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();
Cependant, il existe de nombreux scénarios dans lesquels cela n'est pas idéal pour l'utilisateur du générateur, en particulier si le générateur n'a pas seulement des setters, mais aussi des additionneurs, ou si l'utilisateur ne peut pas contrôler l'ordre dans lequel certaines propriétés du générateur seront disponibles.
La seule solution à laquelle je pourrais penser est très compliquée: pour chaque combinaison de propriétés obligatoires ayant été définies ou n'ayant pas encore été définies, j'ai créé une classe de constructeur dédiée qui sait quels autres paramètres obligatoires doivent être appelés avant d'arriver à un indiquer où la build()
méthode doit être disponible, et chacun de ces setters retourne un type de générateur plus complet qui est un peu plus près de contenir une build()
méthode.
J'ai ajouté le code ci-dessous, mais vous pourriez dire que j'utilise le système de type pour créer un FSM qui vous permet de créer un Builder
, qui peut être transformé en un BuilderWithEngine
ou BuilderWithTransmission
, qui peuvent ensuite être transformés en un CompleteBuilder
, qui implémente lebuild()
méthode. Les setters facultatifs peuvent être invoqués sur n'importe laquelle de ces instances de générateur.
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;
}
};
}
}
}
Comme vous pouvez le constater, cela ne se met pas à l'échelle correctement, car le nombre de classes de générateur différentes requises serait O (2 ^ n) où n est le nombre de setters obligatoires.
D'où ma question: cela peut-il être fait de manière plus élégante?
(Je cherche une réponse qui fonctionne avec Java, bien que Scala soit également acceptable)
.engine(e)
deux fois pour un constructeur?
build()
si vous n'avez pas appelé engine(e)
et transmission(t)
avant.
Engine
implémentation par défaut , puis la remplacer par une implémentation plus spécifique. Mais le plus probable que ce serait plus logique si engine(e)
était pas un poseur, mais un aspic addEngine(e)
. Cela serait utile pour un Car
constructeur qui peut produire des voitures hybrides avec plus d'un moteur / moteur. Puisqu'il s'agit d'un exemple artificiel, je ne suis pas entré dans les détails sur les raisons pour lesquelles vous pourriez vouloir faire cela - par souci de concision.
this
?