web-dev-qa-db-fra.com

Le modèle de générateur et un grand nombre de paramètres obligatoires

À ce jour, j'utilise l'implémentation suivante du modèle de générateur (par opposition à l'implémentation décrite ici ):

public class Widget {
    public static class Builder {
        public Builder(String name, double price) { ... }
        public Widget build() { ... }
        public Builder manufacturer(String value) { ... }
        public Builder serialNumber(String value) { ... }
        public Builder model(String value) { ... }
    }

    private Widget(Builder builder) { ... }
}

Cela fonctionne bien pour la plupart des situations que j'ai rencontrées où j'ai besoin de construire un objet complexe avec une variété de paramètres obligatoires/obligatoires et facultatifs. Cependant, j'ai eu du mal ces derniers temps à comprendre en quoi le modèle est utile lorsque tous vos paramètres sont obligatoires (ou du moins la grande majorité le sont).

Un moyen de contourner ce problème a été de regrouper logiquement les paramètres transmis à leurs propres classes afin de réduire le nombre de paramètres transmis au constructeur du générateur.

Par exemple, au lieu de:

Widget example = new Widget.Builder(req1, req2, req3,req4,req5,req6,req7,req8)
                           .addOptional(opt9)
                           .build();

se regroupe comme suit:

Object1 group1 = new Object1(req1, req2, req3, req4);
Object2 group2 = new Object2(req5, req6);

Widget example2 = new Widget.Builder(group1, group2, req7, req8)
                            .addOptional(opt9)
                            .build();

Bien qu'avoir des objets séparés simplifie un peu les choses, cela rend également les choses un peu difficiles à suivre si l'on ne connaît pas le code. Une chose que j'ai envisagée était de déplacer tous les paramètres dans leurs propres méthodes addParam(param) puis d'effectuer la validation sur les paramètres requis dans la méthode build().

Quelle est la meilleure pratique et y a-t-il peut-être une meilleure approche à cela que je n'ai pas envisagée?

54
speedRS

Cependant, j'ai eu du mal ces derniers temps à comprendre en quoi le modèle est utile lorsque tous vos paramètres sont obligatoires (ou du moins la grande majorité le sont).

Le modèle de constructeur fluide est toujours bénéfique:

  1. Son plus lisible - il permet effectivement des paramètres nommés afin que l'appel ne soit pas seulement une longue liste d'arguments sans nom

  2. Son non ordonné - cela vous permet de regrouper les arguments en groupes logiques, soit dans le cadre d'un appel de setter de générateur unique, soit simplement en vous permettant d'utiliser un ordre naturel pour appeler les méthodes de setter de générateur qui donnent le plus de sens à cette instanciation particulière.


Widget example = new Widget.Builder(req1, req2, req3,req4,req5,req6,req7,req8)
                               .addOptional(opt9)
                               .build();

se regroupe comme suit:

Object1 group1  = new Object1(req1, req2, req3, req4);
Object2 group2  = new Object2(req5, req6);
Widget example2 = new Widget.Builder(group1, group2, req7, req8)
                            .addOptional(opt9)
                            .build();

Bien qu'avoir des objets séparés simplifie un peu les choses, cela rend également les choses un peu difficiles à suivre si l'on ne connaît pas le code. Une chose que j'ai envisagée était de déplacer tous les paramètres dans leurs propres méthodes addParam(param) puis d'effectuer la validation sur les paramètres requis dans la méthode build().

Je préférerais un hybride lorsque cela est approprié ou naturel. Il n'est pas nécessaire que tout soit dans le constructeur o chaque paramètre possède sa propre méthode addParam. Builder vous donne la flexibilité de faire l'un, l'autre, entre les deux ou un combo:

Widget.Builder builder = new Widget.Builder(Widget.BUTTON);

builder.withWidgetBackingService(url, resource, id);
builder.withWidgetStyle(bgColor, lineWidth, fontStyle);
builder.withMouseover("Not required");

Widget example = builder.build();
28
Bert F

Vous pouvez utiliser un Step Builder si vous avez de nombreux paramètres obligatoires. En bref: vous définissez une interface pour chaque paramètre obligatoire unique et une méthode de générateur renvoie la prochaine interface de générateur obligatoire ou le générateur lui-même pour les méthodes facultatives. Le constructeur reste une seule classe qui implémente toutes les interfaces.

Des langages comme Kotlin et Scala sont plus pratiques ici, car ils offrent des paramètres nommés avec des valeurs par défaut.

35
deamon

Un avantage du modèle de générateur que je vois rarement (voire jamais) promu est qu'il peut également être utilisé pour construire conditionnellement l'objet, par exemple uniquement si tous les paramètres obligatoires sont corrects ou si d'autres ressources requises sont disponibles. À cet égard, ils offrent des avantages similaires à un méthode d'usine statique .

3
Toenex

J'ai eu du mal ces derniers temps à comprendre à quel point le modèle est utile lorsque tous vos paramètres sont obligatoires

Le modèle facilite la création de classes immuables et favorise le code lisible. Considérez la classe Person ci-dessous (avec un constructeur conventionnel et un constructeur).

public static class Person {

    private static final class Builder {
        private int height, weight, age, income, rank;
        public Builder setHeight(final int height) { this.height = height; return this; }
        public Builder setWeight(final int weight) { this.weight = weight; return this; }
        public Builder setAge(final int age) { this.age = age; return this; }
        public Builder setIncome(final int income) {    this.income = income; return this; }
        public Builder setRank(final int rank) { this.rank = rank; return this; }
        public Person build() { return new Person(this); }
    }

    private final int height;
    private final int weight;
    private final int age;
    private final int income;
    private final int rank;

    public Person(final int height, final int weight, final int age, final int income, final int rank) {
        this.height = height; this.weight = weight; this.age = age; this.income = income; this.rank = rank;
    }

    private Person(final Builder builder) {
        height = builder.height; weight = builder.weight; age = builder.age; income = builder.income; rank = builder.rank;
        // Perform validation
    }

    public int getHeight() { return height; }
    public int getWeight() { return weight; }
    public int getAge() { return age; }
    public int getIncome() { return income; }
    public int getRank() {  return rank; }

}

Quelle méthode de construction est plus facile à comprendre?

final Person p1 = new Person(163, 184, 48, 15000, 23);
final Person p2 = new Person.Builder().setHeight(163).setWeight(184).setAge(48).
    setIncome(15000).setRank(23).build();

Un moyen de contourner ce problème a été de regrouper logiquement les paramètres transmis à leurs propres classes.

Bien sûr, c'est le principe de cohésion et devrait être adopté quelle que soit la sémantique de construction des objets.

3
hoipolloi

Un constructeur/usine vous permet toujours de dissocier l'interface du type d'implémentation (ou de brancher un adaptateur, etc.), en supposant que Widget devient une interface et que vous avez un moyen d'injecter ou de masquer le new Widget.Builder.

Si vous ne vous souciez pas du découplage et que votre implémentation est unique, alors vous avez raison: le modèle de générateur n'est pas beaucoup plus utile qu'un constructeur normal (il étiquette toujours ses arguments avec l'attribut par générateur - style méthode.)

Si vous créez à plusieurs reprises des objets avec peu de variation dans les arguments, cela peut toujours être utile. Vous pouvez transmettre, mettre en cache, etc. le générateur intermédiaire acquis après avoir branché plusieurs attributs:

Widget.Builder base = new Widget.Builder(name, price).model("foo").manufacturer("baz");

// ...

Widget w1 = base.serialNumber("bar").build();
Widget w2 = base.serialNumber("baz").build();
Widget w3 = base.serialNumber("quux").build();

Cela suppose que vos générateurs sont immuables: les générateurs de générateur ne définissent pas d'attribut et retournent this, mais renvoient une nouvelle copie d'eux-mêmes avec la modification. Comme vous l'avez souligné ci-dessus, les objets paramètres sont un autre moyen de contourner les arguments répétitifs. Là, vous n'avez même pas besoin du modèle de générateur: passez simplement l'objet paramètre à votre constructeur d'implémentation.

0
phs