Java: Comment implémenter un générateur d'étapes pour lequel l'ordre des setters n'a pas d'importance?

10

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 Carinterface 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 Carmust have Engineet Transmissionmais a Stereoest facultatif. Cela signifie qu'un générateur qui peut build()une Carinstance ne devrait avoir une build()méthode que si un Engineet Transmissionont 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 Carinstance sans Engineou 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 BuilderWithEngineou 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. entrez la description de l'image ici

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)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)

derabbink
la source
1
Qu'est-ce qui vous empêche d'utiliser simplement un conteneur IoC pour supporter toutes ces dépendances? De plus, je ne sais pas pourquoi, si l'ordre n'a pas d'importance comme vous l'avez affirmé, vous ne pouvez pas simplement utiliser des méthodes de définition ordinaires qui reviennent this?
Robert Harvey
Que signifie invoquer .engine(e)deux fois pour un constructeur?
Erik Eidt
3
Si vous voulez le vérifier statiquement sans écrire manuellement une classe pour chaque combinaison, vous devez probablement utiliser des trucs au niveau du cou comme des macros ou des métaprogrammations de modèles. Java n'est pas assez expressif pour cela, à ma connaissance, et l'effort n'en vaut probablement pas la peine par rapport à des solutions vérifiées dynamiquement dans d'autres langages.
Karl Bielefeldt
1
Robert: L'objectif est que le vérificateur de type applique le fait qu'un moteur et une transmission sont obligatoires; de cette façon, vous ne pouvez même pas appeler build()si vous n'avez pas appelé engine(e)et transmission(t)avant.
derabbink
Erik: Vous voudrez peut-être commencer par une Engineimplé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 Carconstructeur 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.
derabbink

Réponses:

3

Vous semblez avoir deux exigences différentes, basées sur les appels de méthode que vous avez fournis.

  1. Un seul moteur (requis), une seule transmission (requise) et une seule chaîne stéréo (en option).
  2. Un ou plusieurs moteurs (requis), une ou plusieurs transmissions (requises) et une ou plusieurs chaînes stéréo (facultatives).

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:

public class CarBuilder {

    public CarBuilder(Engine engine, Transmission transmission) {
        // ...
    }

    public CarBuilder setStereo(Stereo stereo) {
        // ...
        return this;
    }
}

et

public class CarBuilder {

    public CarBuilder(List<Engine> engines, List<Transmission> transmission) {
        // ...
    }

    public CarBuilder addStereo(Stereo stereo) {
        // ...
        return this;
    }
}

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.

Zymus
la source
2

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.

public final class CarImpl implements Car {
    private final Engine engine;
    private final Transmission transmission;
    private final Stereo stereo;

    public CarImpl(Engine engine, Transmission transmission) {
        this(engine, transmission, new DefaultStereo());
    }

    public CarImpl(Engine engine, Transmission transmission, Stereo stereo) {
        this.engine = engine;
        this.transmission = transmission;
        this.stereo = stereo;
    }

    //...

}
Pointé
la source
C'est ce que j'ai pensé tout de suite. bien que je n'aurais pas le constructeur à trois paramètres, juste le constructeur à deux paramètres qui a les éléments obligatoires et ensuite un setter pour la stéréo car il est facultatif.
Encaitar
1
Dans un exemple simple (artificiel) comme 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.
derabbink
1
@derabbink Pourquoi ne pas casser votre classe en plus petits dans ce cas? L'utilisation d'un générateur masquera simplement le fait que la classe en fait trop et est devenue impossible à maintenir.
Repéré le
1
Félicitations pour mettre fin à la folie du motif.
Robert Harvey
@Spotted certaines classes contiennent juste beaucoup de données. Par exemple, si vous souhaitez produire une classe de journal d'accès qui contient toutes les informations associées sur la demande HTTP et produire les données au format CSV ou JSON. Il y aura beaucoup de données et si vous souhaitez faire en sorte que certains champs soient présents pendant la compilation, vous aurez besoin d'un modèle de générateur avec un constructeur de liste d'arguments très long, ce qui ne semble pas bon.
ssgao
1

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.

new Builder(e, t).build();                      // ok
new Builder(e, t).stereo(s).build();            // ok
new Builder(e, t).stereo(s).stereo(s).build();  // exception on second call to stereo as stereo is already set 

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.

Pete Kirkham
la source
0

le nombre de classes de générateur différentes requises serait O (2 ^ n) où n est le nombre de setters obligatoires.

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; un nentier 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 des nbits 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.

rwong
la source