[JAVA] Versuchs- und Fehlernotiz für die fließende Schnittstelle

Was ist eine fließende Schnittstelle?

Fließende Schnittstelle | Martin Fowlers Bliki (ja)

Eine fließende Schnittstelle ist eine Technik, die eine Methodenkette verwendet, um einen DSL-ähnlichen Mechanismus zu implementieren.

In bekannten Beispielen mockito, AssertJ, [jOOQ](https: / /www.jooq.org/) und in Federkonfigurationen verwendet.

Beispiel für die Einstellung der Federsicherheit


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

6.2 HttpSecurity | Spring Security Reference

** Geben Sie bei der Implementierung mit einer fließenden Schnittstelle an **

fluent.gif

Verwenden Sie in einer Fluidschnittstelle eine Methodenkette, um die Konstruktion eines Objekts zu deklarieren. Zu diesem Zeitpunkt wird die Methode, die als nächstes aufgerufen werden kann, durch den von jeder Methode zurückgegebenen Typ gesteuert. Infolgedessen wird der als nächstes einzustellende Inhalt durch den Typ angezeigt, und die falsche Einstellung kann vermieden werden.

Unterschied zu einem Methodenketten-Builder

Zum Beispiel, wenn Sie die folgende Klasse haben:

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;
    }
}

Diese "Ziel" -Klasse hat zwei Felder, "foo" und "bar". foo ist ein Pflichtfeld, das im Konstruktor angegeben werden muss, während bar optional ist.

Angenommen, Sie erstellen einen Builder dieser Klasse wie folgt. [^ 1]

[^ 1]: Ich habe es zur Erklärung zu einer einfachen Klasse gemacht, daher werde ich die Geschichte hinterlassen, ob es überhaupt notwendig ist, einen Builder in dieser Klasse zu erstellen.

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;
    }
}

Mit diesem Builder können Sie eine Instanz der Target-Klasse mithilfe einer Methodenkette erstellen. Dieser Builder weist jedoch die folgenden Probleme auf.

Es kann eine falsche Instanz erstellt werden


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

Mit anderen Worten, dieser Builder kann nur in die Methodenkette geschrieben werden und kann die Regeln zum Erstellen der Zielklasse überhaupt nicht ausdrücken.

Andererseits zeigt oder begrenzt die Fluidschnittstelle die folgenden Einstellungen, indem der von der Methode zurückgegebene Typ gesteuert wird. Es gibt verschiedene Möglichkeiten, dies zu erstellen. Angenommen, Sie haben einen Builder wie folgt erstellt.

Builder mit einer fließenden Schnittstelle


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

Dieser Builder erlaubt nur das Initiieren von Deklarationen mit der Methode foo (). Dies ermöglicht es auszudrücken, dass "foo" wesentlich ist.

Außerdem hat der nächste Builder, den foo () zurückgibt, BarBuilder, nur die Methodenbar ()undbuild (). Daher hat der Implementierer keine andere Wahl, als eine dieser Methoden aufzurufen (sie kann nicht mit foo deklariert werden).

Sie können "bar ()" verwenden, um "bar" zu setzen, und "build ()", um die Deklaration zu beenden, ohne "bar" zu setzen. Dies drückt auch aus, dass "bar" willkürlich ist.

Auf diese Weise hat die fließende Schnittstelle die Eigenschaft, dass auch die Konstruktionsregeln des Zielobjekts ausgedrückt werden können.

verdienen

Die Vorteile der Fluidgrenzfläche können wie folgt zusammengefasst werden.

[^ 2]: Beim Versuch, eine Methode aufzurufen, die im Rückgabetyp usw. nicht vorhanden ist.

Fehler

Es gibt nicht nur Vor-, sondern auch Nachteile.

Insbesondere ist es schwierig, die dritte Klasse zu entwerfen, und wenn versucht wird, einen Builder zu erstellen, der bis zu einem gewissen Grad kompliziert ist, wird die Struktur oft kompliziert und der Grund wird nicht verstanden (ich selbst).

Daher werde ich meine eigenen Prozeduren und Hinweise zum Entwerfen und Implementieren einer Klasse organisieren, die eine fließende Schnittstelle ohne Verwirrung realisiert.

Zeichne einen Fluss

Selbst wenn Sie plötzlich mit der Implementierung beginnen, wird es nur verwirrend. Ich denke, es ist eine gute Idee, zuerst eine Reihe von Flows zu zeichnen, die Sie mit einer fließenden Schnittstelle realisieren möchten.

Solange Sie Buchstaben und Pfeile zeichnen können, können Sie Text oder Excel verwenden. Ich verwende oft astah, daher versuche ich, den folgenden Ablauf anhand des Aktivitätsdiagramms zu zeichnen. [^ 3]

[^ 3]: Ich verwende nur das Aktivitätsdiagramm und es ist mir egal, ob es als Aktivitätsdiagramm genau richtig ist.

fluent.jpg

In dieser Abbildung wird der Ablauf gezeichnet, der den Prüfinhalt einfacher Eingabeelemente definiert.

Arten von Einstellungen

Die Einstellungen selbst können je nach zu erstellendem Objekt variieren. Die Typen können jedoch grob in "Definition" oder "Auswahl" eingeteilt werden (glaube ich).

"Definition" erfolgt durch Einstellen eines bestimmten Wertes "Auswahl" bedeutet die Bestimmung des Durchflusses.

Das Festlegen, ob es erforderlich ist oder nicht, fühlt sich möglicherweise wie "Auswahl" an, da Sie entweder "Erforderlich" oder "Optional" ausgewählt haben. Unabhängig davon, was Sie auswählen, ändert sich der nachfolgende Ablauf nicht. Betrachten Sie es daher als eine "Definition", die "erforderliche" oder "beliebige" Werte festlegt.

Andererseits scheint es, dass der Elementtyp auf einen der Werte "Datum", "numerischer Wert" und "Zeichenfolge" gesetzt ist, aber da sich der nachfolgende Ablauf erheblich ändert, wird er als "Auswahl" betrachtet.

Diese beiden Typen weisen die folgenden Unterschiede in der Implementierung auf.

Da "Definition" den Wert festlegt, wird der Wert im Methodenargument übergeben. Andererseits verfügt "Auswahl" über mehrere Methoden, die die Optionen darstellen, und der Rückgabewert jeder Methode bestimmt den nachfolgenden Ablauf.

Bild der Umsetzung


builder
    .required(true)     //"Definition" ob erforderlich
    .number()           //Elementtyp auswählen
    .decimal()          //"Wählen", ob es sich um eine Ganzzahl oder eine kleine Zahl handelt
    .precision(5, 2)    //Genauigkeit "definieren"
    .geaterThan(0.0)    //Definieren Sie den Mindestwert
    .lessThan(10000.0); //Maximalwert "definieren"

** "Definition" hat jedoch Argumente und "Auswahl" hat keine Argumente **. Zur Vereinfachung der Implementierung ist es möglich, mehrere aufeinanderfolgende "Auswahlen" oder "Auswahlen" und die "Definition" unmittelbar danach zu einer zu kombinieren.

Zum Beispiel denke ich, dass es im obigen Beispiel eine Ameise wäre, die Auswahl des Elementtyps, die Auswahl von Ganzzahlen / kleinen Zahlen und die Definition der Genauigkeit unmittelbar danach zusammenzufassen. [^ 4]

[^ 4]: Es ist nicht immer möglich, zusammenzusetzen, und wenn Sie es zusammenzwingen, kann es schwierig sein, die Bedeutung zu verstehen (von Fall zu Fall).

Wenn die Auswahl und die Definition unmittelbar danach zusammengefasst werden


//Im Falle einer kleinen Anzahl
builder
    .required(true)
    .decimal(5, 2) //Elementtypauswahl / Ganzzahl/Fassen Sie die Auswahl / Genauigkeit der Minderheiten zusammen
    .greaterThan(0.0)
    .lessThan(10000.0);

//Für ganze Zahlen
builder
    .required(true)
    .integer() //Elementtypauswahl / Ganzzahl/Fassen Sie die Auswahl der Minderheiten zusammen
    .greaterThan(0)
    .lessThan(10000);

//Für Datum
builder
    .required(true)
    .date()
    .greaterThanEqual(2000, 1, 1)
    .lessThanEqual(2100, 12, 31);

Im Fall einer Definition mit begrenzten Auswahlmöglichkeiten kann es auch einfacher sein, so viele Methoden vorzubereiten, wie es Auswahlmöglichkeiten gibt.

Beispiel für das Ausdrücken von Definitionsoptionen mit Methoden


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() {}
}

In diesem Beispiel werden die spezifischen Klassendefinitionen, die "SomeStrategy" zugewiesen sind, durch die Methoden "foo ()" und "bar ()" getrennt.

Es kann auch vorgenommen werden, eine Instanz als Argument zu akzeptieren, z. B. "Strategie (SomeStrategy)". Durch die Trennung nach Methoden entfällt jedoch die Notwendigkeit einer spezifischen Instanzerstellung auf der Benutzerseite, und Optionen werden auch in der Liste der Methodenabschlüsse angezeigt, wodurch die Angabe vereinfacht wird.

In einigen Fällen kann es jedoch für den Aufrufer besser sein, die Instanz "SomeStrategy" vorzubereiten. (Sie müssen die aus dem DI-Container usw. erhaltene Instanz übergeben.)

Es gibt keine bessere Wahl zwischen Argumentspezifikation und Methodenspezifikation, und ich denke, dass die beste von Fall zu Fall ausgewählt wird.

Mach einen Baumeister

Verwenden Sie die innere Klasse

Bild des Baumeisters unter Verwendung der inneren Klasse


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() {}
}

Für eine Fluidschnittstelle müssen Sie verschiedene Builderklassen entsprechend dem Ablauf vorbereiten. Um ein Objekt endgültig zu erstellen, müssen die von den Builder-Klassen festgelegten Werte gemeinsam genutzt werden.

Wenn alle diese Builderklassen in einer Datei und einer Klasse definiert sind, ist die Anzahl der Dateien groß, und es muss eine Implementierung geschrieben werden, die eine Containerklasse für die gemeinsame Nutzung von Werten erstellt und diese im Konstruktor des Builders weiterleitet. Es neigt dazu, lästig zu sein.

Das Definieren der Builder-Klasse als innere Klasse erleichtert daher die Implementierung (glaube ich). Mit der inneren Klasse können Sie den Wert über das Instanzfeld der äußeren Klasse freigeben, sodass die Containerklasse nicht für die gemeinsame Nutzung von Werten weitergeleitet werden muss.

Im obigen Beispiel sind "FugaBuilder" und "PiyoBuilder" als innere Klassen von "TargetBuilder" definiert. Der von jedem Builder gemeinsam genutzte Wert wird im Instanzfeld von "TargetBuilder" definiert.

Innere Klassen werden normalerweise nicht sehr oft verwendet, daher ist es möglicherweise unangenehm, Implementierungen wie "builder.new FugaBuilder ()" und "TargetBuilder.this" zum ersten Mal zu sehen. Da es sich jedoch um eine richtige Java-Grammatik handelt, muss ich mich daran gewöhnen.

Builder starten

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() {}
}

Um den Builder zu starten, starten Sie plötzlich "Definition" und "Auswahl" mit der "statischen" Factory-Methode, die etwas am wenigsten beschrieben wird und deren Erscheinungsbild einfach ist.

Ich denke, es ist eine Ameise, nach dem Generieren mit "new TargetBuilder ()" oder einem Konstruktor normal zu beginnen, aber die Beschreibung wird etwas langweilig (persönlicher Eindruck).

Ausgang des Erbauers

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() {}
}

Das Ende des Builders gibt normalerweise das erstellte Objekt zurück.

Ich denke, es gibt zwei Muster, wie man das Ende beschreibt.

  1. Beenden Sie mit einer Methode, die das Erstellen des Objekts steuert, z. B. "build ()"
  2. Geben Sie das erstellte Objekt zurück, sobald die endgültigen Einstellungen abgeschlossen sind, ohne build () usw. einzufügen (Implementierungsbeispiel oben).

Die Methode zum Beenden mit "build ()" hat die Funktion, dass "die Einstellung durch den Builder und das Erstellen des Objekts separat ausgeführt werden können". Der Benutzer muss es nur vom Builder festlegen können. Wenn das erstellte Objekt nur auf der Framework-Seite verwendet wird, ist dies möglicherweise besser.

Im Gegenteil, die Methode zum Zurückgeben eines Objekts, sobald die endgültige Einstellung ohne "build ()" abgeschlossen ist, weist eine Funktion auf, die die Beschreibung vereinfacht, wenn der Benutzer das auf der Benutzerseite erstellte Objekt sofort verwenden möchte. (Ein spezielles Beispiel ist der später beschriebene Fall der "Wiederverwendung")

Das Beenden mit "build ()" kann auch die Einheit der API wert sein.

Welche Sie wählen sollen, wird von Fall zu Fall entschieden.

Obligatorischer Definitionsfluss

fluent.jpg

Builder-Implementierung


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() {}
}

Nutzungsbild


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

Bereiten Sie für den Definitionsfluss, der festgelegt werden muss, einen Builder für jede Definition vor und verketten Sie ihn.

Es mag mühsam sein, für jede Definition einen Builder zu erstellen, aber ich denke, dass dies einen großen Vorteil darin hat, auszudrücken, dass es sich um eine "erforderliche Einstellung" handelt.

Auswahlablauf

Basic

fluent.jpg

Builder-Implementierung


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() {}
}

Nutzungsbild


//foo Route auswählen
builder
    .before()
    .fooFlow()
    .foo("FOO")
    .after();

//bar Route auswählen
builder
    .before()
    .barFlow()
    .bar("BAR")
    .after();

Im Fall "Auswahl" wird ein Builder eingefügt, der die Methode der Wahl bereitstellt.

Grundsätzlich wird nur eine "Auswahl" des Flusses durchgeführt, und eine "Definition" wird vom folgenden Builder durchgeführt. Es kann jedoch einfacher sein, die Beschreibung zu reduzieren, wenn die "Definition" unmittelbar danach zusammen durchgeführt wird. (Wenn es schwierig wird, die Bedeutung zu verstehen, wenn Sie sie zusammenzwingen, ist es besser, sie vorsichtig zu teilen.)

Wenn die "Definition" unmittelbar danach zusammengefasst wird


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() {}
}

Nutzungsbild


//foo Route auswählen
builder
    .before()
    .foo("FOO")
    .after();

//bar Route auswählen
builder
    .before()
    .bar("BAR")
    .after();

Optionaler Durchfluss

fluent.jpg

Builder-Implementierung


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() {}
}

Nutzungsbild


//foo Route auswählen
builder
    .before()
    .fooFlow()
    .foo("FOO")
    .after();

//foo Route nicht auswählen
builder
    .before()
    .and()
    .after();

Wenn es eine Route gibt, die willkürlich ausgewählt werden kann, gibt es eine Route, die nichts bewirkt. Bereiten Sie in diesem Fall eine Methode vor, die nichts mit und () zu tun hat, und verketten Sie sie mit dem nächsten Builder.

Wenn die willkürliche Auswahl jedoch kontinuierlich ist, besteht die Gefahr, dass sie je nach Einstellung zu ".und (). Und ()" wird. Verwenden Sie in diesem Fall einen Methodennamen wie "notFoo ()" oder "defaultFoo ()", der das Gefühl ausdrückt, den Standard auszuwählen, um die Ungeschicklichkeit zu verringern.

Schleifenfluss

Single Definition-Schleife

fluent.jpg

Builder-Implementierung


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() {}
}

Nutzungsbild


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

Sie benötigen eine Schleife, wenn Sie ein Objekt mit einer Listenstruktur erstellen möchten. Erstens sind in einem einfachen Fall die Einstellungen in der Schleife nur eine einzige Definition.

FooListBuilder fügt der Liste mit foo (String) ein Element hinzu und gibt dann seine eigene Referenz zurück. Dies ermöglicht das Hinzufügen sich wiederholender Elemente.

Da es jedoch nicht möglich ist, die Schleife so wie sie ist zu verlassen, wird auch eine und () Methode bereitgestellt, um die Schleife zu verlassen.

Mehrfachdefinitionsschleife

fluent.jpg

Builder-Implementierung


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() {}
}

Nutzungsbild


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

Etwas wurde plötzlich kompliziert.

Wenn zum Erstellen jedes Elementobjekts der Liste mehrere Definitionen erforderlich sind, ist diese Mehrfachdefinitionsschleife erforderlich.

Bei einer Schleife mit mehreren Definitionen ist es erforderlich, die in einer Schleife festgelegten Informationen vorübergehend aufzuzeichnen. Zum Zeitpunkt des Endes einer Schleife ist es dann erforderlich, ein Objekt unter Verwendung der aufgezeichneten Informationen zu konstruieren und in eine Liste aufzunehmen.

TargetListBuilder


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

TargetListBuilder hat ein Instanzfeld, so dass die in einer Schleife festgelegten Informationen vorübergehend aufgezeichnet werden können.

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);
        }
    }
    ...

Wenn die Methode "BarBuilder.bar ()" die Definition von "bar" abgeschlossen hat, gibt sie die äußere Klasse "TargetListBuilder" zurück. TargetListBuilder definiert die foo () Methode [^ 5], die weiterhin die nächste Liste erstellt, und dieand ()Methode, die die Listenkonstruktion beendet, sodass Sie eine der beiden auswählen können. Es gibt.

[^ 5]: Anstatt das nächste foo plötzlich zu setzen, ist es möglich, eine Methode wie FooBuilder next () zu verwenden und einen Schritt einzufügen.

Unabhängig von Ihrer Auswahl wird die Methode "save ()" intern aufgerufen, um das Objekt "Target" unter Verwendung der zu diesem Zeitpunkt vorübergehend aufgezeichneten Informationen zur Liste hinzuzufügen.

Wiederverwendung

fluent.jpg

Methode mit generischem Typ

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() {}
}

Nutzungsbild


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

Es wurde auch kompliziert.

Selbst wenn die Flows und Builder unterschiedlich sind, möchten einige möglicherweise genau den gleichen Flow teilen. Wenn Sie beispielsweise ein Objekt erstellen, das jedes Element einer Liste ist, kann es verlockend sein, den Fluss des Teils, der das Objekt erstellt, in einen anderen Builder zu unterteilen und an anderer Stelle wiederzuverwenden.

In diesem Fall lautet die erste Frage "Wie werden die Methodenketten nach dem Split Builder verbunden?". Da die nachfolgende Verarbeitung je nach Aufrufer unterschiedlich ist, kann der Builder für geteilte Wiederverwendungsziele selbst nicht bekannt sein.

Um dies zu erreichen, wird ein generischer Typ verwendet.

SomeBuilder


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

Der Typparameter "" wird deklariert und als Rückgabewert von "bar" verwendet. Dies ist der letzte Prozess von "SomeBuilder". Der "SomeBuilder" selbst erfordert keine Verarbeitung für dieses Objekt vom Typ "AFTER", sondern behält es lediglich als Builder bei, der als nächstes verarbeitet werden soll, und gibt es schließlich zurück.

Der spezifische Name des Typs "AFTER" wird vom Anrufer angegeben.

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 {
        ...
    }

    ...
}

Bei Verwendung mit "TargetBuilder" folgt auf "SomeBuilder" "AfterBuilder". Geben Sie daher "AfterBuilder" für "" von "SomeBuilder" an. Übergeben Sie dann eine Instanz von "AfterBuilder" im Konstruktor von "SomeBuilder".

Auf diese Weise kann SomeBuilder je nach Aufrufer Methodenketten verbinden, ohne den spezifischen Typ des nächsten Builders zu kennen.

Die nächste Frage ist, wie die vom Split Builder beim Aufrufer generierten Informationen empfangen werden. Es gibt mehrere mögliche Methoden, aber hier verwenden wir die Methode über eine Schnittstelle namens "OuterBuilder".

OuterBuilder


package fluent;

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

Eine Methode namens "receive (T)" ist in "OuterBuilder" definiert, damit sie den Wert des durch das type-Argument angegebenen Typs empfangen kann. Dieser "OuterBuilder" wird vom Builder implementiert, der den Builder für die Wiederverwendung verwendet.

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() {}
}

Hier implementiert TargetBuilder`` OuterBuilder. Im Konstruktorargument von "SomeBuilder" übergibt "TargetBuilder.this" dann eine eigene Instanz.

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;
        ...
    }
}

Wenn in "SomeBuilder" die Reihe von Flows abgeschlossen und an den Aufrufer zurückgegeben wird (wenn die "bar ()" - Methode aufgerufen wird), wird das "Some" -Objekt erstellt, das mit "receive ()" von "OuterBuilder" erstellt wurde. Ich übergebe es.

Ein Mechanismus, der diese generischen Typen verwendet, ermöglicht die Wiederverwendung eines Builders an mehreren Standorten.

So erhalten Sie ein mit Argumenten erstelltes Objekt

Die generische Typmethode macht die Methodenkette ununterbrochen und elegant, erschwert jedoch die Implementierung. Wenn andererseits die Methodenkette unterbrochen werden kann, besteht eine einfachere Methode darin, das konstruierte Objekt als Argument zu empfangen.

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() {}
}

Nutzungsbild


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

Bereiten Sie auf der Seite "TargetBuilder" nur den Port ("some ()" - Methode) vor, der das "Some" -Objekt empfängt, das vom wiederverwendeten "SomeBuilder" erstellt wurde. Beim Aufruf der Methode some () wird das Objekt Some unter Verwendung von SomeBuilder erstellt.

Die Methodenkette ist unterbrochen und Sie müssen die foo () Methode von SomeBuilder`` static importieren, aber der Mechanismus ist einfacher als die generische Typmethode.

Wenn Sie diese Methode verwenden, sieht es meiner Meinung nach besser aus, wenn "SomeBuilder" einen Stil annimmt, der nicht mit "build ()" endet.

Erstellen Sie verschachtelte Objekte

fluent.jpg

Builder-Implementierung


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() {}
}

Nutzungsbild


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

Dies ist ein Problem, das eher mit der Struktur des zu erstellenden Objekts als mit dem Fluss zusammenhängt.

Ein Fall, in dem das zu erstellende Objekt ein anderes Objekt als Feld hat und mehrere "Definitionen" oder "Auswahl" erforderlich sind, um das Objekt zu erstellen. In diesem Beispiel hat das zu erstellende Zielobjekt ein anderes Objekt namens "Some" als Feld, und die Konstruktion erfordert zwei Schritte, "foo ()" und "bar ()".

Wie bei Schleifen ist es erforderlich, den Einstellungswert vorübergehend aufzuzeichnen, während das verschachtelte Objekt festgelegt wird. Wenn die Anzahl klein ist, kann sie im Instanzfeld des äußersten Builders (in diesem Fall "TargetBuilder") aufgezeichnet werden. Mit zunehmender Anzahl wird es jedoch schwierig zu wissen, welches Feld für welches verschachtelte Objekt geeignet ist. Es ist schwer.

Daher wird eine innere Klasse für das verschachtelte Objekt eingefügt und die Positionsinformationen für das verschachtelte Objekt werden dort verwaltet. Im Beispiel hier entspricht SomeBuilder dieser Klasse.

Das Instanzfeld der äußersten Klasse enthält nur das konstruierte Objekt.

Implementierungsbeispiel

Basierend auf den bisherigen Mustern können Sie, wenn Sie sie kombinieren und nach Bedarf Feineinstellungen vornehmen, eine allgemein flüssige Schnittstelle implementieren (glaube ich).

Implementieren wir also tatsächlich einen Builder, der den oben als Beispiel angegebenen Elementdefinitionsfluss realisiert. Hier wird der Fluss durch Einbeziehen der Schleife definiert.

** Zu realisierender Fluss **

fluent.jpg

Das von diesem Builder erstellte Objekt sollte eine "Liste" einer Klasse namens "FieldSpec" sein.

** FieldSpec-Struktur **

fluent.jpg

Implementierung

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() {}
}

Anwendungsbeispiel


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

Es sieht aus wie das.

Recommended Posts

Versuchs- und Fehlernotiz für die fließende Schnittstelle
Schnittstelle und Zusammenfassung
Maven3-Fehlerprotokoll
Vererbung und Schnittstelle.
[Java] Klassifizierungsnotiz für Kompilierungsfehler und Laufzeitfehler
abstrakt (abstrakte Klasse) und Schnittstelle (Schnittstelle)
Java-Lernnotiz (Schnittstelle)
Gemeinsame Verarbeitung und Fehlerverarbeitung springmvc