In many cases Java DSL is just an way to assemble some complex configuration and then pass built structure to internal method which will handle it. For example, SQL query builders, HTTP request builders are all fall into this category.
First of all, plain Builder
pattern rarely convenient in such cases. It has no restrictions on order of calls and may result to incomplete configuration or just unreadable code.
The solution is to split Builder
into set of interface chaining each other.
To illustrate approach let's imagine that we're trying to write yet another, very simple HTTP request builder. With described approach it might look like this:
public interface RequestBuilder {
static Stage1 get(String uri) {
return new Builder(Method.GET, uri, list());
}
interface Stage1 {
default Stage2 withoutParameters(Object... parameters) {
return with();
}
Stage2 with(Object... parameters);
}
interface Stage2 {
Request build();
}
class Builder implements Stage1, Stage2 {
private final Method method;
private final String uri;
private final List<?> parameters;
private Builder(final Method method, final String uri, final List<?> parameters) {
this.method = method;
this.uri = uri;
this.parameters = parameters;
}
@Override
public Stage2 with(final Object... parameters) {
return new Builder(method, uri, list(parameters));
}
@Override
public Request build() {
return new Request(method, uri, parameters);
}
}
}
The building code is concentrated in Builder
class. This implementation uses immutable instance, but this is not strictly necessary.
The whole Builder
class is never exposed directly to user code. Instead we return interface which limits user actions to with()
and withoutParameters()
. These methods in turn return next building stage interface (Stage2
) which allows user to finish building object.
The usage of the example code might look like below.
Note that there is no way to not specify parameters (or lack of them) explicitly:
...
RequestBuilder.get("http://somewhere.com/api/users/{param1}")
.with(userId1)
.build();
...
RequestBuilder.get("http://somewhere.com/api/users")
.withoutParameters()
.build();
...
The example is quite simple, but actually there might be any number of intermediate stages and they not necessarily should fix building process as unidirectional graph (for example, creating loops to assemble complex sub-object can be easily done as well).
What we get in turn:
- There is no way to build incomplete object
- User is guided through object building stages by IDE and limited number of choices at each step
- All invocations through the code follow same pattern, making reading and reviewing such a code simpler
- Whole API being properly designed and named can form quite convenient to use DSL.