[JAVA] Mémo d'essai et d'erreur d'interface fluide

Qu'est-ce qu'une interface fluide?

Interface fluide | Bliki de Martin Fowler (ja)

Une interface fluide est une technique qui utilise une chaîne de méthodes pour implémenter un mécanisme de type DSL.

Les exemples familiers incluent mockito, AssertJ et [jOOQ](https: / /www.jooq.org/), et utilisé dans les configurations Spring.

Exemple de paramètre de sécurité Spring


protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests(authorizeRequests ->
            authorizeRequests
                .anyRequest().authenticated()
        )
        .formLogin(withDefaults())
        .httpBasic(withDefaults());
}

6.2 HttpSecurity | Spring Security Reference

** État lors de la mise en œuvre avec une interface fluide **

fluent.gif

Dans une interface fluide, utilisez une chaîne de méthodes pour déclarer la construction d'un objet. À ce stade, la méthode qui peut être appelée ensuite est contrôlée par le type renvoyé par chaque méthode. Par conséquent, le contenu à définir ensuite est indiqué par le type et un paramètre incorrect peut être évité.

Différence par rapport à un simple générateur de chaîne de méthodes

Par exemple, si vous avez la classe suivante:

Target


package nofluent;

public class Target {
    private final String foo;
    private String bar;

    public Target(String foo) {
        this.foo = foo;
    }

    public void setBar(String bar) {
        this.bar = bar;
    }

    public String getFoo() {
        return foo;
    }

    public String getBar() {
        return bar;
    }
}

Cette classe Target a deux champs, foo et bar. foo est un champ obligatoire qui doit être spécifié dans le constructeur, tandis que bar est facultatif.

Supposons que vous créez un générateur de cette classe comme suit. [^ 1]

[^ 1]: J'en ai fait une classe simple pour l'explication, donc je vais laisser l'histoire de savoir s'il est nécessaire de créer un constructeur dans cette classe en premier lieu.

TargetBuilder


package nofluent;

public class TargetBuilder {
    private String foo;
    private String bar;
    
    public static TargetBuilder builder() {
        return new TargetBuilder();
    }
    
    public TargetBuilder foo(String value) {
        this.foo = value;
        return this;
    }
    
    public TargetBuilder bar(String value) {
        this.bar = value;
        return this;
    }
    
    public Target build() {
        Target target = new Target(this.foo);
        if (this.bar != null) {
            target.setBar(this.bar);
        }
        return target;
    }
}

Ce générateur vous permet de créer une instance de la classe Target en utilisant une chaîne de méthodes. Cependant, ce générateur a les problèmes suivants.

--Je ne sais pas que foo est requis --Je ne sais pas que bar est facultatif

Une mauvaise instance peut être créée


Target target = TargetBuilder.builder().bar("BAR").bar("BUZZ").build();

En d'autres termes, ce générateur ne peut être écrit que dans la chaîne de méthodes et ne peut pas du tout exprimer les règles de construction de la classe Target.

D'autre part, l'interface fluide affiche ou limite les paramètres suivants en contrôlant le type renvoyé par la méthode. Il peut y avoir différentes façons de le faire, mais disons que vous avez créé un constructeur comme suit.

Builder avec une interface fluide


package fluent.example;

public class TargetBuilder {
    private String foo;
    
    public static BarBuilder foo(String value) {
        TargetBuilder builder = new TargetBuilder();
        builder.foo = value;
        return builder.new BarBuilder();
    }
    
    public class BarBuilder {
        
        public Target bar(String value) {
            Target target = new Target(foo);
            target.setBar(value);
            return target;
        }
        
        public Target build() {
            return new Target(foo);
        }
    }
    
    private TargetBuilder() {}
}
Target barIsNotNull = TargetBuilder.foo("FOO").bar("BAR");
Target barIsNull = TargetBuilder.foo("FOO").build();

Ce générateur permet uniquement de lancer les déclarations avec la méthode foo (). Cela permet d'exprimer que «foo» est essentiel.

De plus, le constructeur suivant que foo () retourne, BarBuilder, n'a que les méthodesbar ()etbuild (). Par conséquent, l'implémenteur n'a pas d'autre choix que d'appeler l'une de ces méthodes (elle ne peut pas être déclarée avec foo).

Vous pouvez utiliser bar () pour définir bar, et utiliser build () pour terminer la déclaration sans définir bar. Cela exprime également que «bar» est arbitraire.

De cette manière, l'interface fluide a la caractéristique que même les règles de construction de l'objet cible peuvent être exprimées.

mérite

Les avantages de l'interface fluide peuvent être résumés comme suit.

[^ 2]: lorsque vous essayez d'appeler une méthode qui n'existe pas dans le type de retour, etc.

Démérite

Il n'y a pas seulement des avantages mais aussi des inconvénients.

En particulier, il est difficile de concevoir la troisième classe, et lorsque vous essayez de créer un constructeur qui est compliqué dans une certaine mesure, la structure devient souvent compliquée et la raison n'est pas comprise (moi-même).

Par conséquent, je vais organiser mes propres procédures et astuces pour concevoir et implémenter une classe qui réalise une interface fluide sans confusion.

Dessinez un flux

Même si vous commencez à l'implémenter soudainement, cela ne fera que dérouter, donc je pense que c'est une bonne idée de dessiner d'abord une série de flux que vous souhaitez réaliser avec une interface fluide.

Tant que vous pouvez dessiner des lettres et des flèches, vous pouvez utiliser du texte ou Excel. J'utilise souvent astah, donc j'essaye de dessiner le flux suivant en utilisant le diagramme d'activité. [^ 3]

[^ 3]: J'utilise juste le diagramme d'activités, et je m'en fiche s'il est exactement correct en tant que diagramme d'activités.

fluent.jpg

Dans cette figure, le flux qui définit le contenu de vérification des éléments d'entrée simples est dessiné.

Types de paramètres

Les paramètres eux-mêmes peuvent varier en fonction de l'objet à construire. Cependant, les types peuvent être largement classés en «définition» ou «sélection» (je pense).

La "définition" consiste à définir une valeur spécifique «Sélection» signifie déterminer le débit du flux.

Définir s'il est obligatoire ou non peut sembler être une "sélection" car vous avez sélectionné "obligatoire" ou "facultatif". Cependant, quel que soit votre choix, le flux suivant ne change pas. Par conséquent, considérez-le comme une "définition" qui fixe des valeurs "requises" ou "arbitraires".

D'un autre côté, il peut sembler que le type d'élément est défini sur l'une des valeurs «date», «valeur numérique» et «chaîne de caractères», mais comme le flux ultérieur change de manière significative, il est considéré comme «sélection».

Ces deux types apparaissent avec les différences d'implémentation suivantes.

Puisque "definition" définit la valeur, la valeur est passée dans l'argument de méthode. D'autre part, la "sélection" a plusieurs méthodes qui représentent les options, et la valeur de retour de chaque méthode détermine le flux suivant.

Image de mise en œuvre


builder
    .required(true)     //"Définition" si nécessaire
    .number()           //Sélectionnez le type d'élément
    .decimal()          //"Sélectionnez" s'il s'agit d'un entier ou d'un petit nombre
    .precision(5, 2)    //"Définir" la précision
    .geaterThan(0.0)    //"Définir" la valeur minimale
    .lessThan(10000.0); //"Définir" la valeur maximale

Cependant, ** "definition" a des arguments et "selection" n'a aucun argument **. Pour simplifier la mise en œuvre, il est possible de combiner plusieurs "sélections" ou "sélections" consécutives et la "définition" immédiatement après celles-ci en une seule.

Par exemple, dans l'exemple ci-dessus, je pense qu'il serait ant de résumer la sélection du type d'élément, la sélection des nombres entiers / petits et la définition de la précision immédiatement après cela. [^ 4]

[^ 4]: Il n'est pas toujours possible de mettre ensemble, et si vous le forcez ensemble, il peut être difficile d'en comprendre le sens (au cas par cas).

Lorsque la sélection et la définition immédiatement après sont résumées


//Dans le cas d'un petit nombre
builder
    .required(true)
    .decimal(5, 2) //Sélection du type d'élément / entier/Résumer la sélection des minorités / la définition de la précision
    .greaterThan(0.0)
    .lessThan(10000.0);

//Pour les entiers
builder
    .required(true)
    .integer() //Sélection du type d'élément / entier/Résumer les sélections minoritaires
    .greaterThan(0)
    .lessThan(10000);

//Pour date
builder
    .required(true)
    .date()
    .greaterThanEqual(2000, 1, 1)
    .lessThanEqual(2100, 12, 31);

De plus, dans le cas d'une définition avec des choix limités, il peut être plus simple de préparer autant de méthodes qu'il y a de choix.

Exemple d'expression de choix de définition avec des méthodes


package fluent;

public class TargetBuilder {
    private SomeStrategy strategy;
    
    public class SomeStrategyBuilder {
        public AfterBuilder foo() {
            strategy = new FooStrategy();
            return new AfterBuilder();
        }
        
        public AfterBuilder bar() {
            strategy = new BarStrategy();
            return new AfterBuilder();
        }
    }
    
    public class AfterBuilder {...}
    
    private TargetBuilder() {}
}

Dans cet exemple, les définitions de classe spécifiques affectées à SomeStrategy sont séparées par les méthodes foo () ʻet bar ()`.

Il peut également être fait pour accepter une instance comme argument, tel que stratégie (SomeStrategy). Cependant, la séparation par méthode élimine le besoin de création d'instances spécifiques du côté utilisateur, et des options apparaissent également dans la liste des complétions de méthode, ce qui facilite la spécification.

Cependant, dans certains cas, il peut être préférable pour l'appelant de préparer l'instance SomeStrategy. (Vous devez passer l'instance obtenue à partir du conteneur DI, etc.)

Il n'y a pas de meilleur choix entre la spécification d'argument et la spécification de méthode, et je pense que le meilleur sera choisi au cas par cas.

Faire un constructeur

Utiliser la classe interne

Image du constructeur utilisant la classe interne


package fluent;

public class TargetBuilder {
    private String hoge;
    private String fuga;
    private String piyo;
    
    public static FugaBuilder hoge(String value) {
        TargetBuilder builder = new TargetBuilder();
        builder.hoge = value;
        return builder.new FugaBuilder();
    }
    
    public class FugaBuilder {
        
        public PiyoBuilder fuga(String value) {
            fuga = value;
            return new PiyoBuilder();
        }
    }
    
    public class PiyoBuilder {
        
        public TargetBuilder piyo(String value) {
            piyo = value;
            return TargetBuilder.this;
        }
    }
    
    public Target build() {
        return new Target(hoge, fuga, piyo);
    }
    
    private TargetBuilder() {}
}

Pour une interface fluide, vous devez préparer différentes classes de constructeur en fonction du flux. De plus, pour finalement créer un objet, il est nécessaire de partager les valeurs définies par ses classes de générateur.

Si toutes ces classes de générateur sont définies dans un fichier et une classe, le nombre de fichiers sera important et il sera nécessaire d'écrire une implémentation qui crée une classe de conteneur pour partager les valeurs et l'achemine dans le constructeur du générateur. Cela a tendance à être gênant.

Par conséquent, définir la classe de générateur comme une classe interne facilite la mise en œuvre (je pense). Avec la classe interne, les valeurs peuvent être partagées à l'aide des champs d'instance de la classe externe, il n'est donc pas nécessaire d'acheminer une classe de conteneur pour le partage de valeur.

Dans l'exemple ci-dessus, «FugaBuilder» et «PiyoBuilder» sont définis comme des classes internes de «TargetBuilder». La valeur partagée entre chaque générateur est définie dans le champ d'instance de TargetBuilder.

Les classes internes ne sont généralement pas utilisées très souvent, donc vous pouvez trouver désagréable de voir des implémentations telles que builder.new FugaBuilder () et TargetBuilder.this pour la première fois. Cependant, comme il s'agit d'une grammaire Java appropriée, je dois m'y habituer.

Démarrer le constructeur

package fluent;

public class TargetBuilder {
    private String foo;
    
    public static AfterBuilder foo(String value) {
        TargetBuilder builder = new TargetBuilder();
        builder.foo = value;
        return builder.new AfterBuilder();
    }
    
    public class AfterBuilder {...}
    
    private TargetBuilder() {}
}

Pour démarrer le générateur, démarrez soudainement "définition" et "sélection" avec la méthode d'usine "statique", qui est un peu la moindre description et l'apparence est simple.

Je pense qu'il est ant de commencer après avoir généré avec new TargetBuilder () ou un constructeur normalement, mais la description devient un peu terne (impression personnelle).

Sortie du constructeur

package fluent;

public class TargetBuilder {
    private String foo;
    
    public class BeforeBuilder {
        public FooBuilder before() {
            return new FooBuilder();
        }
    }
    
    public class FooBuilder {
        public Target foo(String value) {
            foo = value;
            return new Target(foo);
        }
    }
    
    private TargetBuilder() {}
}

La fin du générateur renvoie généralement l'objet construit.

Je pense qu'il y a deux modèles dans la façon de décrire la fin.

  1. Terminez par une méthode qui dirige la construction de l'objet, telle que build ()
  2. Renvoyez l'objet construit dès que les paramètres finaux sont terminés, sans insérer build () etc. (exemple d'implémentation ci-dessus)

La méthode de fin avec build () a la caractéristique que "le réglage par le constructeur et la construction de l'objet peuvent être exécutés séparément". L'utilisateur n'a besoin que de pouvoir le définir par le constructeur, et si l'objet construit est utilisé uniquement du côté du framework, cela peut être mieux.

D'autre part, la méthode de retour d'un objet dès que les paramètres finaux sont terminés sans build () a la particularité que la description devient simple lorsque vous souhaitez utiliser immédiatement l'objet construit par l'utilisateur. (Un exemple spécifique est le cas de "réutilisation" décrit plus loin)

De plus, terminer par build () peut valoir l'unité de l'API.

Lequel choisir sera au cas par cas.

Flux de définition obligatoire

fluent.jpg

Implémentation du constructeur


package fluent;

public class TargetBuilder {
    private String foo;
    private String bar;
    
    public class BeforeBuilder {
        public FooBuilder before() {
            return new FooBuilder();
        }
    }
    
    public class FooBuilder {
        public BarBuilder foo(String value) {
            foo = value;
            return new BarBuilder();
        }
    }
    
    public class BarBuilder {
        public AfterBuilder bar(String value) {
            bar = value;
            return new AfterBuilder();
        }
    }
    
    public class AfterBuilder {
        public void after() {...}
    }
    
    private TargetBuilder() {}
}

Image d'utilisation


builder
    .before()
    .foo("FOO")
    .bar("BAR")
    .after();

Pour le flux de définition à définir, préparez un générateur pour chaque définition et chaînez-le.

Il peut être difficile de créer un générateur pour chaque définition, mais je pense que cela présente un grand avantage en exprimant qu'il s'agit d'un "paramètre obligatoire".

Flux de sélection

De base

fluent.jpg

Implémentation du constructeur


package fluent;

public class TargetBuilder {
    private String foo;
    private String bar;
    
    public class BeforeBuilder {
        public SelectFooBarBuilder before() {
            return new SelectFooBarBuilder();
        }
    }
    
    public class SelectFooBarBuilder {
        public FooBuilder fooFlow() {
            return new FooBuilder();
        }
        
        public BarBuilder barFlow() {
            return new BarBuilder();
        }
    }
    
    public class FooBuilder {
        public AfterBuilder foo(String value) {
            foo = value;
            return new AfterBuilder();
        }
    }
    
    public class BarBuilder {
        public AfterBuilder bar(String value) {
            bar = value;
            return new AfterBuilder();
        }
    }
    
    public class AfterBuilder {
        public void after() {...}
    }
    
    private TargetBuilder() {}
}

Image d'utilisation


//foo Sélectionnez l'itinéraire
builder
    .before()
    .fooFlow()
    .foo("FOO")
    .after();

//bar Sélectionnez un itinéraire
builder
    .before()
    .barFlow()
    .bar("BAR")
    .after();

Dans le cas de la «sélection», un constructeur qui fournit la méthode de choix est inséré.

Fondamentalement, seule la "sélection" du flux est effectuée et la "définition" est effectuée par le constructeur suivant. Cependant, il peut être plus simple de réduire la description si la «définition» immédiatement après est faite ensemble. (S'il devient difficile de comprendre le sens si vous le forcez ensemble, il vaut mieux le diviser doucement)

Lorsque la "définition" immédiatement après est résumée


package fluent;

public class TargetBuilder {
    private String foo;
    private String bar;
    
    public class BeforeBuilder {
        public SelectFooBarBuilder before() {
            return new SelectFooBarBuilder();
        }
    }
    
    public class SelectFooBarBuilder {
        public AfterBuilder foo(String value) {
            foo = value;
            return new AfterBuilder();
        }
        
        public AfterBuilder bar(String value) {
            bar = value;
            return new AfterBuilder();
        }
    }
    
    public class AfterBuilder {
        public void after() {...}
    }
    
    private TargetBuilder() {}
}

Image d'utilisation


//foo Sélectionnez l'itinéraire
builder
    .before()
    .foo("FOO")
    .after();

//bar Sélectionnez un itinéraire
builder
    .before()
    .bar("BAR")
    .after();

Flux optionnel

fluent.jpg

Implémentation du constructeur


package fluent;

public class TargetBuilder {
    private String foo;
    
    public class BeforeBuilder {
        public SelectFooBuilder before() {
            return new SelectFooBuilder();
        }
    }
    
    public class SelectFooBuilder {
        public FooBuilder fooFlow() {
            return new FooBuilder();
        }
        
        public AfterBuilder and() {
            return new AfterBuilder();
        }
    }
    
    public class FooBuilder {
        public AfterBuilder foo(String value) {
            foo = value;
            return new AfterBuilder();
        }
    }
    
    public class AfterBuilder {
        public void after() {...}
    }
    
    private TargetBuilder() {}
}

Image d'utilisation


//foo Sélectionnez l'itinéraire
builder
    .before()
    .fooFlow()
    .foo("FOO")
    .after();

//foo Ne sélectionnez pas d'itinéraire
builder
    .before()
    .and()
    .after();

S'il y a un itinéraire qui peut être sélectionné arbitrairement, il y aura un itinéraire qui ne fait rien. Dans ce cas, préparez une méthode qui ne fait rien comme ʻand () ʻ et enchaînez-la au constructeur suivant.

Cependant, si la sélection arbitraire est continue, il y a un risque qu'elle devienne «.and (). And ()» selon le paramètre. Dans ce cas, utilisez un nom de méthode tel que notFoo () ou defaultFoo () qui exprime le sentiment de sélectionner la valeur par défaut pour atténuer la maladresse.

Flux de boucle

Boucle de définition unique

fluent.jpg

Implémentation du constructeur


package fluent;

import java.util.ArrayList;
import java.util.List;

public class TargetBuilder {
    private List<String> fooList = new ArrayList<>();
    
    public class BeforeBuilder {
        public FooListBuilder before() {
            return new FooListBuilder();
        }
    }
    
    public class FooListBuilder {
        public FooListBuilder foo(String value) {
            fooList.add(value);
            return this;
        }
        
        public AfterBuilder and() {
            return new AfterBuilder();
        }
    }
    
    public class AfterBuilder {
        public void after() {...}
    }
    
    private TargetBuilder() {}
}

Image d'utilisation


builder
    .before()
        .foo("one")
        .foo("two")
        .foo("three")
        .and()
    .after();

Vous avez besoin d'une boucle si vous souhaitez créer un objet avec une structure de liste. Premièrement, dans un cas simple, les paramètres de la boucle ne sont qu'une seule définition.

FooListBuilder ajoute un élément à la liste avec` foo (String) ʻet retourne ensuite sa propre référence. Cela permet d'ajouter des éléments répétitifs.

Cependant, comme il n'est pas possible de quitter la boucle telle quelle, la méthode ʻand () `est également fournie pour quitter la boucle.

Boucle de définition multiple

fluent.jpg

Implémentation du constructeur


package fluent;

import java.util.ArrayList;
import java.util.List;

public class TargetBuilder {
    private List<Target> targetList = new ArrayList<>();
    
    public class BeforeBuilder {
        public TargetListBuilder.FooBuilder before() {
            return new TargetListBuilder().new FooBuilder();
        }
    }
    
    public class TargetListBuilder {
        private String foo;
        private String bar;
        
        public class FooBuilder {
            public BarBuilder foo(String value) {
                foo = value;
                return new BarBuilder();
            }
        }
        
        public class BarBuilder {
            public TargetListBuilder bar(String value) {
                bar = value;
                return TargetListBuilder.this;
            }
        }
        
        public BarBuilder foo(String value) {
            this.save();
            return new TargetListBuilder().new FooBuilder().foo(value);
        }
        
        public AfterBuilder and() {
            this.save();
            return new AfterBuilder();
        }
        
        private void save() {
            Target target = new Target(foo, bar);
            targetList.add(target);
        }
    }
    
    public class AfterBuilder {
        public void after() {...}
    }
    
    private TargetBuilder() {}
}

Image d'utilisation


builder
    .before()
        .foo("Foo1").bar("Bar1")
        .foo("Foo2").bar("Bar2")
        .foo("Foo3").bar("Bar3")
        .and()
    .after();

Quelque chose est soudainement devenu compliqué.

Si plusieurs définitions sont nécessaires pour créer chaque objet élément de la liste, cette boucle multi-définition est requise.

Dans le cas d'une boucle avec plusieurs définitions, il est nécessaire d'enregistrer temporairement l'ensemble d'informations dans une boucle. Ensuite, au moment où une boucle se termine, il est nécessaire de construire un objet en utilisant les informations enregistrées et de le mettre dans une liste.

TargetListBuilder


    ...
    public class TargetListBuilder {
        private String foo;
        private String bar;
        
        ...
    }
    ...

TargetListBuilder a un champ d'instance afin que les informations définies dans une boucle puissent être temporairement enregistrées.

TargetListBuilder


    ...
    public class TargetListBuilder {
        ...
        public class BarBuilder {
            public TargetListBuilder bar(String value) {
                bar = value;
                return TargetListBuilder.this;
            }
        }
        
        public BarBuilder foo(String value) {
            this.save();
            return new TargetListBuilder().new FooBuilder().foo(value);
        }
        
        public AfterBuilder and() {
            this.save();
            return new AfterBuilder();
        }
        
        private void save() {
            Target target = new Target(foo, bar);
            targetList.add(target);
        }
    }
    ...

Lorsque la méthode BarBuilder.bar () a fini de définir bar, elle retourne la classe externe TargetListBuilder. TargetListBuilder définit la méthode foo () [^ 5] qui continue la construction de liste suivante et la méthode ʻand () `qui termine la construction de la liste afin que vous puissiez sélectionner l'une ou l'autre. Il y a.

[^ 5]: Au lieu de définir soudainement le prochain foo, il est possible d'utiliser une méthode comme FooBuilder next () et d'insérer une étape.

Quel que soit votre choix, la méthode save () est appelée en interne pour ajouter l'objet Target à la liste en utilisant les informations qui ont été temporairement enregistrées à ce moment-là.

Réutilisation

fluent.jpg

Méthode utilisant le type générique

SomeBuilder


package fluent;

public class SomeBuilder<AFTER> {
    private final OuterBuilder<Some> outerBuilder; 
    private final AFTER afterBuilder;
    private String foo;
    
    public class FooBuilder {
        public BarBuilder foo(String value) {
            foo = value;
            return new BarBuilder();
        }
    }
    
    public class BarBuilder {
        public AFTER bar(String bar) {
            Some some = new Some(foo, bar);
            outerBuilder.receive(some);
            
            return afterBuilder;
        }
    }
    
    SomeBuilder(OuterBuilder<Some> outerBuilder, AFTER afterBuilder) {
        this.outerBuilder = outerBuilder;
        this.afterBuilder = afterBuilder;
    }
}

OuterBuilder


package fluent;

public interface OuterBuilder<T> {
    void receive(T t);
}

TargetBuilder


package fluent;

public class TargetBuilder implements OuterBuilder<Some> {
    private Some some;

    public class BeforeBuilder {
        public SomeBuilder<AfterBuilder>.FooBuilder before() {
            SomeBuilder<AfterBuilder> someBuilder =
                new SomeBuilder<>(TargetBuilder.this, new AfterBuilder());
            return someBuilder.new FooBuilder();
        }
    }
    
    public class AfterBuilder {
        public void after() {...}
    }

    @Override
    public void receive(Some some) {
        this.some = some;
    }
    
    private TargetBuilder() {}
}

Image d'utilisation


builder
    .before()
    .foo("FOO")
    .bar("BAR")
    .after();

C'est aussi devenu compliqué.

Même si les flux et les générateurs sont différents, certains peuvent souhaiter partager exactement le même flux. Par exemple, lors de la création d'un objet qui est chaque élément d'une liste, il peut être tentant de diviser le flux de la partie qui crée l'objet dans un autre générateur et de le réutiliser ailleurs.

Dans ce cas, la première question est "comment connecter les chaînes de méthodes après le générateur de fractionnement". Étant donné que le traitement suivant diffère selon l'appelant, le générateur de cible de réutilisation divisée lui-même ne peut pas être connu.

Pour ce faire, un type générique est utilisé.

SomeBuilder


public class SomeBuilder<AFTER> {
    ...
    private final AFTER afterBuilder;
    ...
    
    public class BarBuilder {
        public AFTER bar(String bar) {
            ...
            return afterBuilder;
        }
    }
    ...
}

Le paramètre de type «» est déclaré et utilisé comme valeur de retour de «bar», qui est le dernier processus de «SomeBuilder». Le SomeBuilder lui-même ne nécessite aucun traitement pour cet objet de type ʻAFTER, il le garde juste comme constructeur pour traiter ensuite et finalement retourne`.

Le type spécifique de ʻAFTER` sera spécifié par l'appelant.

TargetBuilder


public class TargetBuilder implements OuterBuilder<Some> {
    ...

    public class BeforeBuilder {
        public SomeBuilder<AfterBuilder>.FooBuilder before() {
            SomeBuilder<AfterBuilder> someBuilder =
                new SomeBuilder<>(..., new AfterBuilder());
            return someBuilder.new FooBuilder();
        }
    }
    
    public class AfterBuilder {
        ...
    }

    ...
}

Lorsqu'il est utilisé avec TargetBuilder, SomeBuilder est suivi de ʻAfterBuilder. Par conséquent, spécifiez ʻAfterBuilder pour <AFTER> ʻof SomeBuilder. Ensuite, passez une instance de ʻAfterBuilder dans le constructeur de SomeBuilder.

Cela permet à SomeBuilder de connecter des chaînes de méthodes en fonction de l'appelant sans connaître le type spécifique du constructeur suivant.

La question suivante est de savoir comment recevoir les informations générées par le générateur de division chez l'appelant. Il existe plusieurs méthodes possibles, mais ici nous utilisons la méthode via une interface appelée ʻOuterBuilder`.

OuterBuilder


package fluent;

public interface OuterBuilder<T> {
    void receive(T t);
}

ʻOuterBuilder définit une méthode appelée receive (T) , qui vous permet de recevoir la valeur du type spécifié par l'argument type. Cet ʻOuterBuilder est implémenté par le générateur qui utilise le générateur de réutilisation.

TargetBuilder


package fluent;

public class TargetBuilder implements OuterBuilder<Some> {
    private Some some;
    
    ...

    public class BeforeBuilder {
        public SomeBuilder<AfterBuilder>.FooBuilder before() {
            SomeBuilder<AfterBuilder> someBuilder =
                new SomeBuilder<>(TargetBuilder.this, ...);
            return someBuilder.new FooBuilder();
        }
    }
    
    public class AfterBuilder {...}

    @Override
    public void receive(Some some) {
        this.some = some;
    }
    
    private TargetBuilder() {}
}

Ici, TargetBuilder implémente ʻOuterBuilder. Ensuite, dans l'argument constructeur de SomeBuilder, TargetBuilder.this` transmet sa propre instance.

SomeBuilder


package fluent;

public class SomeBuilder<AFTER> {
    private final OuterBuilder<Some> outerBuilder; 
    ...
    
    public class BarBuilder {
        public AFTER bar(String bar) {
            Some some = new Some(foo, bar);
            outerBuilder.receive(some);
            
            return afterBuilder;
        }
    }
    
    SomeBuilder(OuterBuilder<Some> outerBuilder, AFTER afterBuilder) {
        this.outerBuilder = outerBuilder;
        ...
    }
}

Dans SomeBuilder, lorsque la série de flux est terminée et renvoyée à l'appelant (lorsque la méthode bar () est appelée), l'objet Some construit en utilisant receive () de ʻOuterBuilder` est créé. Je le remets.

Un mécanisme utilisant ces types génériques permet de réutiliser un générateur à plusieurs endroits.

Comment recevoir un objet construit avec des arguments

La méthode de type générique rend la chaîne de méthodes ininterrompue et élégante, mais elle complique l'implémentation. D'un autre côté, si la chaîne de méthodes peut être rompue, une méthode plus simple consiste à recevoir l'objet construit comme argument.

SomeBuilder


package fluent;

public class SomeBuilder {
    private String foo;
    
    public static BarBuilder foo(String value) {
        SomeBuilder builder = new SomeBuilder();
        builder.foo = value;
        return builder.new BarBuilder();
    }
    
    public class BarBuilder {
        public Some bar(String bar) {
            return new Some(foo, bar);
        }
    }
    
    private SomeBuilder() {}
}

TargetBuilder


package fluent;

public class TargetBuilder {
    private Some some;

    public class BeforeBuilder {
        public SomeReceiveBuilder before() {
            return new SomeReceiveBuilder();
        }
    }
    
    public class SomeReceiveBuilder {
        public AfterBuilder some(Some value) {
            some = value;
            return new AfterBuilder();
        }
    }
    
    public class AfterBuilder {
        
        public void after() {...}
    }
    
    private TargetBuilder() {}
}

Image d'utilisation


builder
    .before()
    .some(foo("FOO").bar("BAR"))
    .after();

Du côté TargetBuilder, ne préparez que le port (méthodesome ()) qui reçoit l'objet Some construit par le SomeBuilder réutilisé. Ensuite, lors de l'appel à la méthode some (), l'objet Some est construit en utilisant SomeBuilder.

La chaîne de méthodes est rompue et vous devez importer la méthode foo () de SomeBuilder`` statique, mais le mécanisme est plus simple que la méthode de type générique.

Si vous utilisez cette méthode, je pense qu'il est préférable que SomeBuilder adopte un style qui ne se termine pas par build ().

Créer des objets imbriqués

fluent.jpg

Implémentation du constructeur


package fluent;

public class TargetBuilder {
    private Some some;

    public class BeforeBuilder {
        public SomeBuilder.FooBuilder before() {
            return new SomeBuilder().new FooBuilder();
        }
    }
    
    public class SomeBuilder {
        private String foo;
        private String bar;
        
        public class FooBuilder {
            public BarBuilder foo(String value) {
                foo = value;
                return new BarBuilder();
            }
        }
        
        public class BarBuilder {
            public AfterBuilder bar(String value) {
                bar = value;
                saveSome();
                return new AfterBuilder();
            }
        }
        
        private void saveSome() {
            some = new Some(foo, bar);
        }
    }
    
    public class AfterBuilder {
        
        public void after() {...}
    }
    
    private TargetBuilder() {}
}

Image d'utilisation


builder
    .before()
    .foo("BOO")
    .bar("BAR")
    .after();

Il s'agit d'un problème lié à la structure de l'objet à construire plutôt qu'au flux.

Un cas où l'objet à construire a un autre objet comme champ et plusieurs «définition» ou «sélection» sont nécessaires pour construire l'objet. Dans cet exemple, l'objet Target à construire a un autre objet appelé Some comme champ, et la construction nécessite deux étapes,foo ()etbar ().

Comme dans le cas des boucles, il est nécessaire d'enregistrer temporairement la valeur de réglage lors de la définition de l'objet imbriqué. Si le nombre est petit, il peut être enregistré dans le champ d'instance du constructeur le plus à l'extérieur (TargetBuilder dans ce cas), mais à mesure que le nombre augmente, il devient difficile de savoir quel champ est la chose pour quel objet imbriqué. C'est dur.

Par conséquent, une classe interne est insérée pour l'objet imbriqué et les informations de position de l'objet imbriqué y sont gérées. Dans l'exemple ici, «SomeBuilder» correspond à cette classe.

Le champ d'instance de la classe la plus externe a uniquement l'objet construit.

Exemple d'implémentation

Sur la base des modèles jusqu'à présent, si vous les combinez et faites des ajustements fins au besoin, vous pouvez implémenter une interface généralement fluide (je pense).

Donc, implémentons en fait un constructeur qui réalise le flux de définition d'élément donné comme exemple ci-dessus. Ici, le flux est défini en incorporant la boucle.

** Flux à réaliser **

fluent.jpg

L'objet construit par ce générateur doit être une List d'une classe appelée FieldSpec.

** Structure de FieldSpec **

fluent.jpg

La mise en oeuvre

FieldSpecListBuilder


package fluent.example;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class FieldSpecListBuilder {
    private List<FieldSpec> list = new ArrayList<>();
    
    public static FieldSpecBuilder.RequiredBuilder field(String name) {
        FieldSpecBuilder builder = new FieldSpecListBuilder().new FieldSpecBuilder(name);
        return builder.new RequiredBuilder();
    }
    
    public class FieldSpecBuilder {
        private final String name;
        private boolean required;
        private FieldSpec fieldSpec;
        
        private FieldSpecBuilder(String name) {
            this.name = name;
        }
        
        public class RequiredBuilder {
            public SelectFieldTypeBuilder required(boolean value) {
                required = value;
                return new SelectFieldTypeBuilder();
            }
        }

        public class SelectFieldTypeBuilder {
            public DateSpecBuilder.DateFormatBuilder date() {
                return new DateSpecBuilder().new DateFormatBuilder();
            }
            
            public IntegerSpecBuilder.MinIntegerBuilder integer() {
                return new IntegerSpecBuilder().new MinIntegerBuilder();
            }
            
            public DecimalSpecBuilder.MinDecimalBuilder decimal(int integerSize, int decimalSize) {
                return new DecimalSpecBuilder(integerSize, decimalSize).new MinDecimalBuilder();
            }
            
            public TextSpecBuilder.TextTypeBuilder text() {
                return new TextSpecBuilder().new TextTypeBuilder();
            }
        }
        
        public class DateSpecBuilder {
            private String format;
            private LocalDate min;
            private LocalDate max;
            
            public class DateFormatBuilder {
                public MinDateBuilder yyyyMMdd() {
                    format = "yyyyMMdd";
                    return new MinDateBuilder();
                }

                public MinDateBuilder yyMMdd() {
                    format = "yyMMdd";
                    return new MinDateBuilder();
                }
            }

            public class MinDateBuilder {
                public MaxDateBuilder greaterThan(int year, int month, int dayOfMonth) {
                    min = LocalDate.of(year, month, dayOfMonth);
                    return new MaxDateBuilder();
                }
            }

            public class MaxDateBuilder {
                public FieldSpecBuilder lessThan(int year, int month, int dayOfMonth) {
                    max = LocalDate.of(year, month, dayOfMonth);
                    saveSpec();
                    return FieldSpecBuilder.this;
                }
            }
            
            private void saveSpec() {
                DateSpec dateSpec = new DateSpec(format, min, max);
                fieldSpec = new FieldSpec(name, required, dateSpec);
            }
        }
        
        public class IntegerSpecBuilder {
            private int min;
            private boolean minInclude;
            private int max;
            private boolean maxInclude;
            
            public class MinIntegerBuilder {
                public MaxIntegerBuilder greaterThan(int value) {
                    min = value;
                    minInclude = false;
                    return new MaxIntegerBuilder();
                }

                public MaxIntegerBuilder greaterThanEqual(int value) {
                    min = value;
                    minInclude = true;
                    return new MaxIntegerBuilder();
                }
            }

            public class MaxIntegerBuilder {
                public FieldSpecBuilder lessThan(int value) {
                    max = value;
                    maxInclude = false;
                    saveSpec();
                    return FieldSpecBuilder.this;
                }

                public FieldSpecBuilder lessThanEqual(int value) {
                    max = value;
                    maxInclude = true;
                    saveSpec();
                    return FieldSpecBuilder.this;
                }
            }
            
            private void saveSpec() {
                IntegerSpec integerSpec = new IntegerSpec(min, minInclude, max, maxInclude);
                fieldSpec = new FieldSpec(name, required, integerSpec);
            }
        }
        
        public class DecimalSpecBuilder {
            private int integerSize;
            private int decimalSize;
            private double min;
            private boolean minInclude;
            private double max;
            private boolean maxInclude;

            private DecimalSpecBuilder(int integerSize, int decimalSize) {
                this.integerSize = integerSize;
                this.decimalSize = decimalSize;
            }
            
            public class MinDecimalBuilder {
                public MaxDecimalBuilder greaterThan(double value) {
                    min = value;
                    minInclude = false;
                    return new MaxDecimalBuilder();
                }

                public MaxDecimalBuilder greaterThanEqual(double value) {
                    max = value;
                    minInclude = true;
                    return new MaxDecimalBuilder();
                }
            }

            public class MaxDecimalBuilder {
                public FieldSpecBuilder lessThan(double value) {
                    max = value;
                    maxInclude = false;
                    saveSpec();
                    return FieldSpecBuilder.this;
                }

                public FieldSpecBuilder lessThanEqual(double value) {
                    max = value;
                    maxInclude = true;
                    saveSpec();
                    return FieldSpecBuilder.this;
                }
            }
            
            private void saveSpec() {
                DecimalSpec decimalSpec = new DecimalSpec(integerSize, decimalSize, min, minInclude, max, maxInclude);
                fieldSpec = new FieldSpec(name, required, decimalSpec);
            }
        }
        
        public class TextSpecBuilder {
            private TextSpec.TextType type;
            private int minLength;
            private int maxLength;
            
            public class TextTypeBuilder {
                public TextMinLengthBuilder textType(TextSpec.TextType value) {
                    type = value;
                    return new TextMinLengthBuilder();
                }
            }

            public class TextMinLengthBuilder {
                public TextMaxLengthBuilder minLength(int value) {
                    minLength = value;
                    return new TextMaxLengthBuilder();
                }
            }

            public class TextMaxLengthBuilder {
                public FieldSpecBuilder maxLength(int value) {
                    maxLength = value;
                    saveSpec();
                    return FieldSpecBuilder.this;
                }
            }
            
            private void saveSpec() {
                TextSpec textSpec = new TextSpec(type, minLength, maxLength);
                fieldSpec = new FieldSpec(name, required, textSpec);
            }
        }
        
        public RequiredBuilder field(String name) {
            save();
            return new FieldSpecBuilder(name).new RequiredBuilder();
        }
        
        public List<FieldSpec> build() {
            save();
            return list;
        }
        
        private void save() {
            list.add(fieldSpec);
        }
    }
    
    private FieldSpecListBuilder() {}
}

Exemple d'utilisation


List<FieldSpec> fieldSpecList =
    FieldSpecListBuilder
            .field("foo")
                .required(false)
                .date()
                    .yyyyMMdd()
                    .greaterThan(2019, 1, 1)
                    .lessThan(2019, 12, 31)
            .field("bar")
                .required(true)
                .integer()
                    .greaterThan(0)
                    .lessThan(100)
            .field("fizz")
                .required(true)
                .decimal(3, 2)
                .greaterThanEqual(0.0)
                .lessThanEqual(100.0)
            .field("buzz")
                .required(false)
                .text()
                .textType(TextSpec.TextType.ALPHABET)
                .minLength(1)
                .maxLength(10)
            .build();

Ça ressemble à ça.

Recommended Posts

Mémo d'essai et d'erreur d'interface fluide
interface et résumé
Mémo d'erreur Maven3
Héritage et interface.
[Java] Mémo de classification d'erreur de compilation et d'erreur d'exécution
abstract (classe abstraite) et interface (interface)
Mémo d'apprentissage Java (interface)
Traitement courant et traitement des erreurs Springmvc