[JAVA] Flowing interface trial and error memo

What is a fluent interface?

Flowing Interface | Martin Fowler's Bliki (ja)

A fluent interface is a technique that uses a method chain to implement a DSL-like mechanism.

As a familiar example, mockito, AssertJ, [jOOQ](https: / /www.jooq.org/), and used in Spring configurations.

Spring Security setting example


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

6.2 HttpSecurity | Spring Security Reference

** State when implementing with a flowing interface **

fluent.gif

In a fluid interface, we use method chains to declare the construction of objects. At this time, the method that can be called next is controlled by the type returned by each method. As a result, the content to be set next is indicated by the type, and the wrong setting can be avoided.

Difference from just a method chain builder

For example, if you have the following class:

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

This Target class has two fields, foo and bar. foo is a required field that must be specified in the constructor, while bar is optional.

Suppose you create a builder of this class as follows. [^ 1]

[^ 1]: I've made it a simple class for explanation, so I'll leave the story of whether it is necessary to create a builder in this class in the first place.

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

This builder allows you to build an instance of the Target class using a method chain. However, this builder has the following problems.

--I don't know that foo is required --I don't know that bar is optional -- foo and bar can be set multiple times

Wrong instance can be built


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

In other words, this builder can only be written in the method chain, and cannot express the rules for building the Target class at all.

On the other hand, the flowing interface shows or limits the following settings by controlling the type returned by the method. There may be various ways to make it, but let's say you made a builder as follows.

Builder with a fluid interface in mind


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

This builder only allows declarations to be initiated with the foo () method. This makes it possible to express that foo is essential.

Also, the next builder that foo () returns, BarBuilder, has only bar () and build () methods. Therefore, the implementer has no choice but to call one of these methods (it cannot be declared with foo).

You can use bar () to set bar, and usebuild ()to end the declaration without setting bar. This also expresses that bar is arbitrary.

In this way, the flowing interface has the characteristic that even the construction rules of the target object can be expressed.

merit

The advantages of a fluid interface can be summarized as follows.

--The next setting is indicated by the type returned by the method. --If you try to write the wrong settings, you will get a compile error [^ 2] --If you are using the IDE, the following settings will be listed by input completion.

[^ 2]: When trying to call a method that does not exist in the return type, etc.

Demerit

There are not only advantages but also disadvantages.

--It would be a shame if you were using automatic code formatting (maybe) --If you're new to this technique, you'll be surprised by the non-Java-like code. ――Designing a builder class is quite difficult

In particular, it is difficult to design the third class, and when trying to create a builder that is complicated to some extent, the structure often becomes complicated and the reason is not understood (I myself).

Therefore, I will organize my own procedures and hints for designing and implementing a class that realizes a flowing interface without confusion.

Draw a flow

Even if you start implementing it suddenly, it will only be confusing, so I think it's a good idea to first draw a series of flows that you want to realize with a flowing interface.

As long as you can draw letters and arrows, you can use text or Excel. I often use astah, so I try to draw the following flow using the activity diagram. [^ 3]

[^ 3]: I'm just using an activity diagram, and I don't care if it's exactly correct as an activity diagram.

fluent.jpg

In this figure, the flow that defines the check contents of simple input items is drawn.

Types of settings

The settings themselves can vary depending on the object to be built. However, the types can be broadly classified into either "definition" or "selection" (I think).

"Definition" is by setting a specific value "Selection" means determining the flow of the flow.

Setting whether it is required or not may feel like "selection" because you have selected either "required" or "optional". However, whichever you choose, the subsequent flow does not change. Therefore, think of it as a "definition" that sets "required" or "arbitrary" values.

On the other hand, it may seem that the item type is set to one of the values "date", "numerical value", and "character string", but since the flow after that changes significantly, it is considered as "selection".

These two types appear with the following differences in implementation.

Since "definition" sets the value, the value is passed in the method argument. On the other hand, "selection" has several methods that represent the options, and the return value of each method determines the subsequent flow.

Image of implementation


builder
    .required(true)     //"Definition" whether required
    .number()           //Select item type
    .decimal()          //"Select" an integer or a decimal
    .precision(5, 2)    //"Define" accuracy
    .geaterThan(0.0)    //"Define" the minimum value
    .lessThan(10000.0); //"Define" the maximum value

However, ** "definition" has arguments and "selection" has no arguments **. In consideration of simplification of implementation, it is possible to combine a plurality of consecutive "selections" or "selections" and the "definition" immediately after that into one.

For example, in the above example, I think it would be ant to summarize the item type selection, integer / decimal selection, and the definition of precision immediately after that. [^ 4]

[^ 4]: It is not always possible to put together, and if you force it together, it may be difficult to understand the meaning (case by case).

When the selection and the definition immediately after are summarized


//In the case of a small number
builder
    .required(true)
    .decimal(5, 2) //Item type selection / integer/Summarize decimal selection / accuracy definitions
    .greaterThan(0.0)
    .lessThan(10000.0);

//For integers
builder
    .required(true)
    .integer() //Item type selection / integer/Summarize minority selections
    .greaterThan(0)
    .lessThan(10000);

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

Also, in the case of a definition with limited choices, it may be simpler to prepare as many methods as there are choices.

Example of expressing definition choices with methods


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 this example, the specific class definitions assigned to SomeStrategy are separated by the methodsfoo ()andbar ().

It can also be made to accept an instance as an argument, such as strategy (SomeStrategy). However, if you divide by method, you do not need to create a specific instance on the user side, and the options will appear in the method completion list and it will be easier to specify.

However, in some cases it may be better for the caller to have a SomeStrategy instance ready. (You have to pass the instance obtained from the DI container, etc.)

There is no better choice between argument specification and method specification, and I think that the most suitable one will be selected on a case-by-case basis.

Make a builder

Use inner class

Image of builder using inner class


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

For a fluid interface, you have to prepare various builder classes according to the flow. Also, in order to finally create an object, it is necessary to share the values set by its builder classes.

If all of these builder classes are defined in one file and one class, the number of files will be large, and it will be necessary to write an implementation that creates a container class for sharing values and routes it in the builder's constructor. It tends to be troublesome.

Therefore, if you define the builder class as an inner class, it will be easier to implement (I think). With an inner class, you can share values using the instance fields of the outer class, so you don't have to route a container class for value sharing.

In the above example, FugaBuilder and PiyoBuilder are defined as inner classes of TargetBuilder. The value shared between each builder is defined in the instance field of TargetBuilder.

Inner classes aren't usually used very often, so you may find it unpleasant to see implementations such as builder.new FugaBuilder () and TargetBuilder.this for the first time. However, since it is a proper Java grammar, I have to get used to it.

Start builder

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

To start the builder, suddenly start "definition" and "selection" with a static factory method, which is somewhat the least amount of description and the appearance is simple.

I think it's ant to start after generating with new TargetBuilder () or a constructor normally, but the description becomes a little dull (personal impression).

Exit of builder

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

The end of the builder usually returns the constructed object.

I think there are two patterns in how to describe the end.

  1. End with a method that directs the building of the object, such as build ()
  2. Do not insert build () etc. and return the constructed object as soon as the final setting is completed (implementation example above)

The method of ending with build () has the feature that "setting by the builder and building of the object can be executed separately". The user side only needs to be able to set by the builder, and if the constructed object is used only on the framework side, this may be better.

On the contrary, the method of returning the object as soon as the final setting is completed without build () has the feature that the description becomes simple when you want to use the object built by the user immediately. (A specific example is the case of "reuse" described later)

Also, the method of ending with build () may be worth the unity of the API.

Which one to choose will be on a case-by-case basis.

Mandatory definition flow

fluent.jpg

Builder implementation


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

Usage image


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

For the definition flow that must be set, prepare a builder for each definition and chain it.

It may be troublesome to create a builder for each definition, but I think that doing so has a great advantage in expressing that it is a "required setting".

Selection flow

Basic

fluent.jpg

Builder implementation


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

Usage image


//foo Select route
builder
    .before()
    .fooFlow()
    .foo("FOO")
    .after();

//bar Select route
builder
    .before()
    .barFlow()
    .bar("BAR")
    .after();

In the "selection" case, there is one builder that provides the method of choice.

Basically, only "selection" of the flow is performed, and "definition" is performed by the following builder. However, it may be simpler to reduce the description if the "definition" immediately after is done together. (If it becomes difficult to understand the meaning if you force it together, it is better to divide it gently)

When the "definition" immediately after is summarized


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

Usage image


//foo Select route
builder
    .before()
    .foo("FOO")
    .after();

//bar Select route
builder
    .before()
    .bar("BAR")
    .after();

Optional flow

fluent.jpg

Builder implementation


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

Usage image


//foo Select route
builder
    .before()
    .fooFlow()
    .foo("FOO")
    .after();

//foo Do not select route
builder
    .before()
    .and()
    .after();

If there is a route that can be selected arbitrarily, there will be a route that does nothing. In that case, prepare a method that does nothing like ʻand () `and chain it to the next builder.

However, if the arbitrary selection is continuous, there is a risk that it will become .and (). And () depending on the setting. In that case, use a method name that expresses the feeling of selecting the default, such as notFoo () or defaultFoo (), to alleviate the clumsiness.

Loop flow

Single definition loop

fluent.jpg

Builder implementation


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

Usage image


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

You need a loop if you want to build an object with some list structure. First, in a simple case, the settings in the loop are only a single definition.

FooListBuilder adds an element to the list withfoo (String)and then returns its own reference. This makes it possible to add repeating elements.

However, since it is not possible to exit the loop as it is, the ʻand () `method is also provided to exit the loop.

Multiple definition loop

fluent.jpg

Builder implementation


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

Usage image


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

Something suddenly became complicated.

If multiple definitions are required to build each element object in the list, this multi-definition loop is needed.

In the case of a loop with multiple definitions, it is necessary to temporarily record the information set in one loop. Then, at the timing when one loop ends, it is necessary to construct an object using the recorded information and put it in a list.

TargetListBuilder


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

TargetListBuilder has an instance field so that the information set in one loop can be temporarily recorded.

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

When the BarBuilder.bar () method finishes defining bar, it returns the outer class TargetListBuilder. TargetListBuilder defines thefoo ()method [^ 5] that continues to build the next list and the ʻand ()` method that ends the list construction so that you can select either one. There is.

[^ 5]: Instead of setting the next foo suddenly, it is possible to use a method likeFooBuilder next ()and insert one step.

Whichever you choose, the save () method is internally called to add the Target object to the list using the information that was temporarily recorded at that time.

Reuse

fluent.jpg

Method using generic type

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

Usage image


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

It also became complicated.

Even if the flows and builders are different, some may want to share the exact same flow. For example, when creating an object that is each element of a list, you may want to divide the flow of the part that creates the object into another builder and reuse it elsewhere.

In this case, the first question is "how to connect the method chains after the split builder". Since the subsequent processing differs depending on the caller, the divided reuse target builder itself cannot be known.

To achieve this, a generic type is used.

SomeBuilder


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

The type parameter <AFTER> is declared and used as the return value of bar, which is the last process of SomeBuilder. The SomeBuilder itself does not require any processing for this ʻAFTER type object, it just keeps it as the builder to process next and finally returns`.

The specific what the ʻAFTER` type will be will be specified by the caller.

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

    ...
}

When used with TargetBuilder, SomeBuilder is followed by ʻAfterBuilder. Therefore, specify ʻAfterBuilder for<AFTER>of SomeBuilder. Then, pass an instance of ʻAfterBuilder in the constructor of SomeBuilder`.

This allows SomeBuilder to connect method chains depending on the caller without knowing the specific type of the next builder.

The next question is how to receive the information generated by the split builder at the caller. There are several possible methods, but here we use the method via an interface called ʻOuterBuilder`.

OuterBuilder


package fluent;

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

ʻOuterBuilder defines a method called receive (T) , which allows you to receive the value of the type specified by the type argument. This ʻOuterBuilder is implemented by the builder who uses the reuse builder.

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

Here, TargetBuilder implements ʻOuterBuilder. Then, in the constructor argument of SomeBuilder, TargetBuilder.this` passes its own 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;
        ...
    }
}

In SomeBuilder, when the series of flows is completed and returned to the caller (when thebar ()method is called), the Some object constructed using receive () of ʻOuterBuilder` is created. I'm handing it over.

A mechanism that uses these generic types makes it possible to reuse one builder in multiple locations.

How to receive an object constructed with arguments

The generic type method makes the method chain uninterrupted and elegant, but it complicates implementation. On the other hand, if the method chain can be broken, a simpler method is to accept the constructed object as an 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() {}
}

Usage image


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

On the TargetBuilder side, prepare only the port (some () method) that receives the Some object constructed by the reused SomeBuilder. Then, in the call to the some () method, the Some object is constructed using SomeBuilder.

The method chain is broken and you need to import the foo () method of SomeBuilder`` static, but the mechanism is simpler than the generic type method.

If you use this method, I think it will look better if SomeBuilder adopts a style that does not end withbuild ().

Create nested objects

fluent.jpg

Builder implementation


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

Usage image


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

This is a problem related to the structure of the object to be built rather than the flow.

A case where the object to be built has another object as a field, and multiple "definition" or "selection" is required to build that object. In this example, the Target object to be built has another object called Some as a field, and its construction requires two steps, foo () and bar ().

As with loops, you need to temporarily record the settings while you set the nested objects. If the number is small, it may be recorded in the instance field of the outermost builder (TargetBuilder in this case), but as the number increases, it becomes difficult to know which field is the thing for which nested object. It's hard.

Therefore, one inner class is inserted for the nested object, and the position information for the nested object is managed there. In the example here, SomeBuilder corresponds to that class.

The instance field of the outermost class has only the constructed object.

Implementation example

Based on the patterns so far, if you combine them and make fine adjustments as needed, you can implement a generally fluid interface (I feel).

So, let's actually implement a builder that realizes the item definition flow given as an example above. Here, the flow is defined by incorporating the loop.

** Flow to be realized **

fluent.jpg

The object built by this builder should be a List of a class called FieldSpec.

** FieldSpec structure **

fluent.jpg

Implementation

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

Usage example


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

It looks like this.

Recommended Posts

Flowing interface trial and error memo
interface and abstract
Maven3 error memo
Inheritance and interface.
[Java] Classification memo of compilation error and run-time error
abstract (abstract class) and interface (interface)
Java learning memo (interface)
Common processing and error processing springmvc
Installing and building Docker (memo)