davidmarquis / fluent-interface-proxy

Forget boiler plate code for your Java Fluent builders! This project provides a proxy that implements your Builder interfaces dynamically (no code required!)
MIT License
55 stars 17 forks source link
builder fluent-interface java java8

1 minute primer

Given a Person bean:

public class Person {
    private String name;
    private int age;
    private Person partner;
    private List<Person> friends;

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

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

    public void setPartner(Person partner) {
        this.partner = partner;
    }

    public void setFriends(List<Person> friends) {
        this.friends = friends;
    }

    ... getters omitted for brevity ...
}

And a builder:

public interface PersonBuilder extends Builder<Person> {
    PersonBuilder withName(String name);
    PersonBuilder withAge(int age);
    PersonBuilder withPartner(PersonBuilder partner);
    PersonBuilder havingFriends(PersonBuilder... friends);
}

Enjoy an automatic implementation of your builder:

public static PersonBuilder aPerson() {
    return ReflectionBuilder.implementationFor(PersonBuilder.class).create();
}

...

Person person = aPerson()
                    .withName("John Doe")
                    .withAge(44)
                    .withPartner( aPerson().withName("Diane Doe") )
                    .havingFriends(
                        aPerson().withName("Smitty Smith"),
                        aPerson().withName("Joe Anderson")
                    )
                    .build();

Yay! No code!

The problem

Writing Fluent Interfaces for creating simple beans in Java is cumbersome. It requires the developer to write a lot of boilerplate code to implement a simple "set properties" type builder. This small project aims at facilitating the implementation of such patterns by providing an automatic implementation of these builder interfaces.

The solution

This library provides a dynamic implementation of your builder interface that will be able to build the target object using the Reflection API. The dynamic implementation is in fact a Proxy that intercepts all calls to your interface and stores property values to set when comes the time to build your final object.

All you need to make sure is that you follow a few conventions when designing your builder interface. Keep reading!

Usage

Compatibility

Starting from version 2, this library is only compatible with Java 8+. Version 1.3.2 was the last version that was compatible with pre Java 8 environments.

Maven dependency

The project is published to Maven Central. To use it in your Maven project, add this dependency to your pom.xml:

<dependency>
    <groupId>com.github.davidmarquis</groupId>
    <artifactId>fluent-interface-proxy</artifactId>
    <version>LATEST</version>
</dependency>

Building from sources

You can build from sources using Maven by running:

mvn clean package

Features

Tips for designing your builder interfaces

Using your own build method

By default, ReflectionBuilder assumes that your builder interfaces extend the Builder interface provided by the library.

If you want to use your own builder interface (and thus your own build() methods), you need to tell the ReflectionBuilder when creating the dynamic builder:

ReflectionBuilder.implementationFor(YourBean.class)
        .withDelegate(new YourBuilderDelegate())
        .create();

Have a look at the BuilderDelegate interface, as well as the default implementation of this interface DefaultBuilderDelegate for more details on what to provide in your own implementation. The abstract base class AbstractBuilderDelegate provides an quick and easy starting point to plug your own builder interfaces into the library.

Choosing between setters or private fields

The library supports both setting the target bean's attributes using public setters or private fields (using the Reflection API). By default, public setters are used. You may choose to use fields directly using this:

ReflectionBuilder.implementationFor(YourBuilder.class)
        .usingFieldsDirectly()
        .create();

You may also provide your own implementation of the AttributeAccessStrategy interface and use it this way:

ReflectionBuilder.implementationFor(YourBean.class)
        .usingAttributeAccessStrategy(new YourStrategy()) // implements AttributeAccessStrategy interface
        .create();

Defining non-standard property-setting methods using the @Sets(property=...) annotation

The library defines a naming convention for property-setting methods. But for more flexibility, it is possible to define your own method names using the Sets annotation:

public interface PersonBuilder extends Builder<Person> {
    @Sets(property = "name")
    PersonBuilder named(String name);
    @Sets(property = "age")
    PersonBuilder aged(String age);
    ...
}

Custom value conversions on setters using @Sets(via=...)

If you need to do special processing of values passed to your setter methods before setting them on the target bean, you can provide your own conversion function using the @Sets annotation:

public interface PersonBuilder extends Builder<Person> {
    @Sets(via = StringToStatus.class)
    PersonBuilder withStatus(String status);

    class StringToStatus implements Function<String, Status> {
        public Status apply(String value) {
            return new Status(value);
        }
    }
}

The Class passed to the via parameter must implement the Java Function interface. A new instance of that class will be created every time the property needs to be set and will be called instead of the library's default processing - which is to try to convert the source value into the destination type on a best effort basis.

Using non-empty constructors

Sometimes the beans you are building may have only non-empty constructors available, or you may require the use of a specific constructor when using your dynamic builder.

Example bean with a non-default constructor:

public class Person {

    private String name;
    private Person spouse;
    private int age;

    public Person(String name, Person spouse) {
        this.name = name;
        this.spouse = spouse;
    }

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

The library supports these non-default constructors using three different mechanisms:

Option 1: @Constructs annotation on builder methods (since 1.2.0)

public interface PersonBuilder extends Builder<Person> {
    @Constructs
    PersonBuilder of(String name, PersonBuilder spouse);
    ...
}

Any method annotated with @Constructs will end up calling the corresponding constructor (if found) on the built object.

Option 2: Varargs in build method (since 1.2.0)

Using the varargs argument constructor in the provided Builder interface:

PersonBuilder aPerson() {
    return ReflectionBuilder.implementationFor(PersonBuilder.class).create();
}

Person person = aPerson().build("Jeremy", aPerson().withAge(16));

As showcased in the above 2 examples, it is very much possible to have builders as parameters and the library will build those builders for you before invoking the class constructors.

The library will check all available constructors on your target bean's class and will find the best matching constructor to use from the types of the parameters you pass to the build(...) method or the @Constructs annotated method.

There are certain limitations however:

Option 3: Using a custom Instantiator when configuring the builder (since 2.1.0)

If the above mechanisms do not fit your need, you can take full control of the instantiation process by providing a custom Instantiator implementation when creating your reflection builder:

PersonBuilder aPerson() {
    return ReflectionBuilder.implementationFor(PersonBuilder.class)
        .usingInstantiator((BuilderState state) -> {
            return new Person(
                state.consume("name", String.class).orElseThrow(() -> new IllegalStateException("Name is required")),
                state.consume("spouse", Person.class).orElse(null)
            );
        })
        .create();
}

The Instantiator interface has a single method instantiate(BuilderState) whose sole responsibility is to create the target object using the right constructor and using the passed BuilderState instance to obtain values for each constructor parameters (as set by previous invocations of property-setting methods).

If you don't want the properties used during the instantiation process to be used later on when setting properties on the object, it is important that the consume() method be used to remove the property from the state after usage.

If on the other hand, you want the builder to continue to consider these properties after instantiation, use the peek() method instead.

Other documentation

Have a look at the tests defined in the test folder to see some sample usages of the dynamic builder.