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)
la source
this
?.engine(e)
deux fois pour un constructeur?build()
si vous n'avez pas appeléengine(e)
ettransmission(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 siengine(e)
était pas un poseur, mais un aspicaddEngine(e)
. Cela serait utile pour unCar
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.Réponses:
Vous semblez avoir deux exigences différentes, basées sur les appels de méthode que vous avez fournis.
Je pense que le premier problème ici est que vous ne savez pas ce que vous voulez que la classe fasse. Une partie de cela est que l'on ne sait pas à quoi vous voulez que l'objet construit ressemble.
Une voiture ne peut avoir qu'un seul moteur et une seule transmission. Même les voitures hybrides n'ont qu'un seul moteur (peut-être un
GasAndElectricEngine
)Je vais aborder les deux implémentations:
et
Si un moteur et une transmission sont nécessaires, ils doivent être dans le constructeur.
Si vous ne savez pas quel moteur ou transmission est requis, n'en définissez pas encore; c'est un signe que vous créez le constructeur trop loin dans la pile.
la source
Pourquoi ne pas utiliser le modèle d'objet nul? Débarrassez-vous de ce générateur, le code le plus élégant que vous puissiez écrire est celui que vous n'avez en réalité pas à écrire.
la source
Car
, cela aurait du sens car le nombre d'arguments c'tor est très petit. Cependant, dès que vous avez affaire à quelque chose de modérément complexe (> = 4 arguments obligatoires), le tout devient plus difficile à gérer / moins lisible ("Le moteur ou la transmission est-il venu en premier?"). C'est pourquoi vous utiliseriez un générateur: l'API vous oblige à être plus explicite sur ce que vous construisez.Premièrement, à moins que vous ayez beaucoup plus de temps que n'importe quel magasin dans lequel j'ai travaillé, cela ne vaut probablement pas la peine de permettre un ordre de fonctionnement ou de vivre avec le fait que vous pouvez spécifier plus d'une radio. Notez que vous parlez de code, pas de saisie utilisateur, vous pouvez donc avoir des assertions qui échoueront pendant votre test unitaire plutôt qu'une seconde avant au moment de la compilation.
Cependant, si votre contrainte est, comme indiqué dans les commentaires, que vous devez avoir un moteur et une transmission, alors imposez-le en mettant toutes les propriétés obligatoires est le constructeur du constructeur.
Si ce n'est que la stéréo qui est facultative, alors la dernière étape en utilisant des sous-classes de constructeurs est possible, mais au-delà de cela, le gain d'obtenir l'erreur au moment de la compilation plutôt que lors des tests ne vaut probablement pas la peine.
la source
Vous avez déjà deviné la bonne direction pour cette question.
Si vous souhaitez obtenir une vérification au moment de la compilation, vous aurez besoin de
(2^n)
types. Si vous souhaitez obtenir une vérification au moment de l'exécution, vous aurez besoin d'une variable qui peut stocker des(2^n)
états; unn
entier de -bit fera l'affaire.Étant donné que C ++ prend en charge les paramètres de modèle non-type (par exemple, les valeurs entières) , il est possible qu'un modèle de classe C ++ soit instancié en
O(2^n)
différents types, en utilisant un schéma similaire à celui-ci .Cependant, dans les langues qui ne prennent pas en charge les paramètres de modèle non-type, vous ne pouvez pas compter sur le système de
O(2^n)
types pour instancier différents types.La prochaine opportunité est avec les annotations Java (et les attributs C #). Ces métadonnées supplémentaires peuvent être utilisées pour déclencher un comportement défini par l'utilisateur au moment de la compilation, lorsque des processeurs d'annotation sont utilisés. Cependant, ce serait trop de travail pour vous de les mettre en œuvre. Si vous utilisez des frameworks qui fournissent cette fonctionnalité pour vous, utilisez-la. Sinon, vérifiez la prochaine opportunité.
Enfin, notez que le stockage de
O(2^n)
différents états en tant que variable au moment de l'exécution (littéralement, comme un entier ayant au moins desn
bits de large) est très facile. C'est pourquoi les réponses les plus votées vous recommandent toutes d'effectuer cette vérification au moment de l'exécution, car l'effort nécessaire pour implémenter la vérification au moment de la compilation est trop important par rapport au gain potentiel.la source