web-dev-qa-db-fra.com

Comment implémenter le modèle de générateur dans Java 8?

Je trouve souvent fastidieux de mettre en œuvre le modèle de générateur avec les configurations antérieures à Java-8. Il y a toujours beaucoup de code presque dupliqué. Le constructeur lui-même pourrait être considéré comme un passe-partout.

En fait, il existe détecteurs de doublons de code , qui considéreraient presque chaque méthode d'un générateur créée avec des fonctionnalités antérieures à Java-8 comme un duplicata de toutes les autres méthodes.

Donc, en considérant la classe suivante et son générateur pré-Java-8:

public class Person {

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public class PersonBuilder {

    private static class PersonState {
        public String name;
        public int age;
    }

    private PersonState  state = new PersonState();

    public PersonBuilder withName(String name) {
        state.name = name;
        return this;
    }

    public PersonBuilder withAge(int age) {
        state.age = age;
        return this;
    }

    public Person build() {
        Person person = new Person();
        person.setAge(state.age);
        person.setName(state.name);
        state = new PersonState();
        return person;
    }
}

Comment le modèle de générateur doit-il être implémenté à l'aide des fonctionnalités Java-8?

15
SpaceTrucker

La GenericBuilder

L'idée de construire objets mutables (les objets immuables sont abordés plus tard) consiste à utiliser des références de méthode aux setters de l'instance à construire. Cela nous conduit à un constructeur générique capable de construire chaque POJO avec un constructeur par défaut - un constructeur pour les gouverner tous ;-)

La mise en œuvre est la suivante:

public class GenericBuilder<T> {

    private final Supplier<T> instantiator;

    private List<Consumer<T>> instanceModifiers = new ArrayList<>();

    public GenericBuilder(Supplier<T> instantiator) {
        this.instantiator = instantiator;
    }

    public static <T> GenericBuilder<T> of(Supplier<T> instantiator) {
        return new GenericBuilder<T>(instantiator);
    }

    public <U> GenericBuilder<T> with(BiConsumer<T, U> consumer, U value) {
        Consumer<T> c = instance -> consumer.accept(instance, value);
        instanceModifiers.add(c);
        return this;
    }

    public T build() {
        T value = instantiator.get();
        instanceModifiers.forEach(modifier -> modifier.accept(value));
        instanceModifiers.clear();
        return value;
    }
}

Le générateur est construit avec un fournisseur qui crée de nouvelles instances, qui sont ensuite modifiées par les modifications spécifiées avec la méthode with.

GenericBuilder serait utilisé pour Person comme ceci:

Person value = GenericBuilder.of(Person::new)
            .with(Person::setName, "Otto").with(Person::setAge, 5).build();

Propriétés et autres utilisations

Mais il y a plus à découvrir sur ce constructeur. 

Par exemple, l'implémentation ci-dessus efface les modificateurs. Cela pourrait être déplacé dans sa propre méthode. Par conséquent, le constructeur conserverait son état entre les modifications et il serait facile de créer plusieurs instances égales. Ou, selon la nature d'une instanceModifier, une liste d'objets variables. Par exemple, une instanceModifier pourrait lire sa valeur à partir d'un compteur croissant.

Poursuivant sur cette idée, nous pourrions implémenter une méthode fork qui renverrait un nouveau clone de l'instance GenericBuilder à laquelle elle est appelée. Cela est facilement possible car l’état du générateur ne contient que instantiator et la liste de instanceModifiers. À partir de là, les deux constructeurs pourraient être modifiés avec un autre instanceModifiers. Ils partageraient la même base et auraient des états supplémentaires définis sur des instances construites.

Le dernier point que je considère particulièrement utile lorsque vous avez besoin d’entités lourdes pour des tests unitaires ou même d’intégration dans des applications d’entreprise. Il n'y aurait pas d'objet divin pour les entités, mais pour les constructeurs.

La GenericBuilder peut également remplacer le besoin de différentes usines de valeur de test. Dans mon projet actuel, de nombreuses usines sont utilisées pour créer des instances de test. Le code est étroitement lié à différents scénarios de test et il est difficile d'extraire des parties d'une usine de test pour les réutiliser dans une autre usine de test dans un scénario légèrement différent. Avec GenericBuilder, la réutilisation devient beaucoup plus facile car il n’existe qu’une liste spécifique de instanceModifiers.

Pour vérifier que les instances créées sont valides, la variable GenericBuilder peut être initialisée avec un ensemble de prédicats vérifiés dans la méthode build une fois que toutes les variables instanceModifiers ont été exécutées.

public T build() {
    T value = instantiator.get();
    instanceModifiers.forEach(modifier -> modifier.accept(value));
    verifyPredicates(value);
    instanceModifiers.clear();
    return value;
}

private void verifyPredicates(T value) {
    List<Predicate<T>> violated = predicates.stream()
            .filter(e -> !e.test(value)).collect(Collectors.toList());
    if (!violated.isEmpty()) {
        throw new IllegalStateException(value.toString()
                + " violates predicates " + violated);
    }
}

Création d'objet immuable

Pour utiliser le schéma ci-dessus pour la création de objets immuables , extrayez l'état de l'objet immuable en un objet mutable et utilisez l'instanciateur et le générateur pour agir sur l'objet d'état mutable. Ajoutez ensuite une fonction qui créera une nouvelle instance immuable pour l'état mutable. Cependant, cela nécessite que l’objet immuable ait son état encapsulé ou soit modifié de cette façon (en appliquant essentiellement un modèle d’objet paramètre à son constructeur).

Ceci est en quelque sorte différent de celui utilisé par un générateur avant Java-8. Là, le générateur lui-même était l’objet mutable qui a créé une nouvelle instance à la fin. Nous avons maintenant une séparation de l'état qu'un constructeur conserve dans un objet mutable et de la fonctionnalité de constructeur elle-même.

Essentiellement
Arrêtez d’écrire des modèles standard et devenez productif avec la variable GenericBuilder.

68
SpaceTrucker

Vous pouvez consulter le projet lombok

Pour votre cas

@Builder
public class Person {
    private String name;
    private int age;
}

Cela générerait le code à la volée

public class Person {
    private String name;
    private int age;
    public String getName(){...}
    public void setName(String name){...}
    public int getAge(){...}
    public void setAge(int age){...}
    public Person.Builder builder() {...}

    public static class Builder {
         public Builder withName(String name){...}
         public Builder withAge(int age){...}
         public Person build(){...}
    }        
}

Lombok le fait lors de la phase de compilation et est transparent pour les développeurs.

7
popcorny
public class PersonBuilder {
    public String salutation;
    public String firstName;
    public String middleName;
    public String lastName;
    public String suffix;
    public Address address;
    public boolean isFemale;
    public boolean isEmployed;
    public boolean isHomewOwner;

    public PersonBuilder with(
        Consumer<PersonBuilder> builderFunction) {
        builderFunction.accept(this);
        return this;
    }


    public Person createPerson() {
        return new Person(salutation, firstName, middleName,
                lastName, suffix, address, isFemale,
                isEmployed, isHomewOwner);
    }
}

Usage

Person person = new PersonBuilder()
    .with($ -> {
        $.salutation = "Mr.";
        $.firstName = "John";
        $.lastName = "Doe";
        $.isFemale = false;
    })
    .with($ -> $.isHomewOwner = true)
    .with($ -> {
        $.address =
            new PersonBuilder.AddressBuilder()
                .with($_address -> {
                    $_address.city = "Pune";
                    $_address.state = "MH";
                    $_address.pin = "411001";
                }).createAddress();
    })
    .createPerson();

Consultez: https://medium.com/beingprofessional/think-functional-advanced-builder-pattern-using-lambda-284714b85ed5

Disclaimer: je suis l'auteur du post

5
Sujit Kamthe

Nous pouvons utiliser l’interface fonctionnelle Consumer de Java 8 pour éviter de multiples méthodes getter/setter.

Reportez-vous au code mis à jour ci-dessous avec l'interface consommateur.

import Java.util.function.Consumer;

public class Person {

    private String name;

    private int age;

    public Person(Builder Builder) {
        this.name = Builder.name;
        this.age = Builder.age;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Person{");
        sb.append("name='").append(name).append('\'');
        sb.append(", age=").append(age);
        sb.append('}');
        return sb.toString();
    }

    public static class Builder {

        public String name;
        public int age;

        public Builder with(Consumer<Builder> function) {
            function.accept(this);
            return this;
        }

        public Person build() {
            return new Person(this);
        }
    }

    public static void main(String[] args) {
        Person user = new Person.Builder().with(userData -> {
            userData.name = "test";
            userData.age = 77;
        }).build();
        System.out.println(user);
    }
}

Reportez-vous au lien ci-dessous pour connaître les informations détaillées avec les différents exemples.

https://medium.com/beingprofessional/think-functional-advanced-builder-pattern-using-lambda-284714b85ed5

https://dkbalachandar.wordpress.com/2017/08/31/Java-8-builder-pattern-with-consumer-interface/

2
Balachandar

J'ai récemment essayé de revoir le modèle de générateur dans Java 8 et j'utilise actuellement l'approche suivante:

public class Person {

    static public Person create(Consumer<PersonBuilder> buildingFunction) {
        return new Person().build(buildingFunction);
    }

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    private Person() {

    }

    private Person build(Consumer<PersonBuilder> buildingFunction) {
        buildingFunction.accept(new PersonBuilder() {

            @Override
            public PersonBuilder withName(String name) {
                Person.this.name = name;
                return this;
            }

            @Override
            public PersonBuilder withAge(int age) {
                Person.this.age = age;
                return this;
            }
        });

        if (name == null || name.isEmpty()) {
            throw new IllegalStateException("the name must not be null or empty");
        }

        if (age <= 0) {
            throw new IllegalStateException("the age must be > 0");
        }

        // check other invariants

        return this;
    }
}

public interface PersonBuilder {

    PersonBuilder withName(String name);

    PersonBuilder withAge(int age);
}

Usage:

var person = Person.create(
    personBuilder -> personBuilder.withName("John Smith").withAge(43)
);

Avantages:

  • Une interface de constructeur propre
  • Peu ou pas de code passe-partout
  • Le constructeur est bien encapsulé
  • Il est facile de séparer les attributs facultatifs des attributs obligatoires de la classe cible (les attributs facultatifs sont spécifiés dans le générateur).
  • Aucun setter nécessaire dans la classe cible (dans DDD, vous ne voulez généralement pas de setters)
  • Utilisation d'une méthode de fabrique statique pour créer une instance de la classe cible (au lieu d'utiliser le nouveau mot-clé, il est donc possible d'avoir plusieurs méthodes de fabrique statique, chacune avec un nom explicite)

Inconvénients possibles:

  • Le code appelant peut enregistrer une référence au générateur transmis et vider ultérieurement l'instance montée, mais qui le fera?
  • Si le code appelant enregistre une référence au générateur transmis, une fuite de mémoire peut se produire.

Alternative possible:

Nous pouvons configurer un constructeur avec une fonction de construction, comme suit:

public class Person {

    static public Person create(Consumer<PersonBuilder> buildingFunction) {
        return new Person(buildingFunction);
    }

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    private Person(Consumer<PersonBuilder> buildingFunction) {
        buildingFunction.accept(new PersonBuilder() {

            @Override
            public PersonBuilder withName(String name) {
                Person.this.name = name;
                return this;
            }

            @Override
            public PersonBuilder withAge(int age) {
                Person.this.age = age;
                return this;
            }
        });

        if (name == null || name.isEmpty()) {
            throw new IllegalStateException("the name must not be null or empty");
        }

        if (age <= 0) {
            throw new IllegalStateException("the age must be > 0");
        }

        // check other invariants
    }
}
0
Stéphane Appercel