I've had some success in the past using fluent API with builders. A fluent interface is implemented by using method chaining to relay the instruction context of a subsequent call. My expectation was to create something like:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Message m = Builder.create().withParam1("A").withParam2("B").build(); |
Some parameters were common to all message types so it made sense to have these in a base class.
The message base class:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
abstract class Message { | |
private final String name; | |
private final String id; | |
private final long timestamp; | |
Message(final MessageBuilder builder) { | |
this.name = builder.getName(); | |
this.id = builder.getId(); | |
this.timestamp = System.currentTimeMillis(); | |
} | |
public String getId() { | |
return id; | |
} | |
public String getName() { | |
return name; | |
} | |
public long getTimestamp() { | |
return timestamp; | |
} | |
} |
and it's associated builder:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public abstract class MessageBuilder<M> { | |
private String name; | |
private String id; | |
public abstract M build(); | |
String getId() { | |
return id; | |
} | |
String getName() { | |
return name; | |
} | |
public MessageBuilder<M> withId(final String id) { | |
this.id = id; | |
return this; | |
} | |
public MessageBuilder<M> withName(final String name) { | |
this.name = name; | |
return this; | |
} | |
} |
I then subclassed the builder class to create the specialized message types. For brevity most parameters have been omitted.
An example of a specialized message class with inherits all the properties of the base class along with its own parameters.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public final class Request extends Message { | |
private final String message; | |
Request(final RequestBuilder builder) { | |
super(builder); | |
this.message = builder.getMessage(); | |
} | |
public String getMessage() { | |
return message; | |
} | |
} |
The builder for this class:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class RequestBuilder extends MessageBuilder<Request> { | |
private String message; | |
public static RequestBuilder create() { | |
return new RequestBuilder(); | |
} | |
private RequestBuilder() { | |
} | |
@Override | |
public Request build() { | |
final Request request = new Request(this); | |
return request; | |
} | |
String getMessage() { | |
return message; | |
} | |
@Override | |
public RequestBuilder withId(final String id) { | |
super.withId(id); | |
return this; | |
} | |
public RequestBuilder withMessage(final String request) { | |
this.message = request; | |
return this; | |
} | |
@Override | |
public RequestBuilder withName(final String name) { | |
super.withName(name); | |
return this; | |
} | |
} |
However to get the expected usage of:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
RequestBuilder.create() | |
.withId("TXID-1") | |
.withName("Request") | |
.withMessage("SELECT temperature FROM sensor") | |
.build(); |
I've had to subclass all withXXX methods from the Message base class. In this example, it's not too painful as there's only a couple of common parameters but more realistically I could have numerous parameters which means for each specialized subclass I need to override those methods. This approach quickly becomes unwieldy and a maintenance headache. To make sure that method chain returns the correct type I've had to resort to calling the super method and then return the correct type i.e this. Not very good.What I needed was for each subclass to inherit the super class methods implicitly but with the proviso that they return the actual type not the super type.
A solution is to use Self Bound GenericTypes. Angelika Langer gives a good explanation which she calls the getThis() trick.
The base MessageBuilder class has been changed to use self bound generic types. So for example, instead of hardwiring
withId()
to return the type of the builder that defines it, a type parameter B
is introduced and withId() returns B
via the abstract self() method. This self method implemented in the subclass to return the concrete type rather than the base type of the builder. The self-referential definition MessageBuilder<B extends MessageBuilder<B>>
allows the return type of the inherited withId()
in RequestBuilder
to be RequestBuilder
rather than MessageBuilder
.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public abstract class MessageBuilder<B extends MessageBuilder<B>>{ | |
private String name; | |
private String id; | |
public abstract Message build(); | |
String getId() { | |
return id; | |
} | |
String getName() { | |
return name; | |
} | |
protected abstract B self(); | |
public B withId(final String id) { | |
this.id = id; | |
return self(); | |
} | |
public B withName(final String name) { | |
this.name = name; | |
return self(); | |
} | |
} |
Now all the subclass has to do, in addition to its own fluent methods, is to provide an implementation of the self() method. Job done.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class RequestBuilder extends MessageBuilder<RequestBuilder> { | |
public static RequestBuilder create() { | |
return new RequestBuilder(); | |
} | |
private String message; | |
private RequestBuilder() { | |
} | |
@Override | |
public Request build() { | |
final Request request = new Request(this); | |
return request; | |
} | |
String getMessage() { | |
return message; | |
} | |
@Override | |
protected RequestBuilder self() { | |
return this; | |
} | |
public RequestBuilder withMessage(final String request) { | |
this.message = request; | |
return this; | |
} | |
} |
In this way, subclasses for builders only need be concerned with providing fluent methods for the parameters pertinent to that class. They will automatically inherit fluent methods from the super class so no overriding is required and the intent of the class is clearer. I can build up that message's fluent methods to set the parameters I actually need for that particular context without resorting to long unwieldy parameter lists. Again the intent is clearer. Result !
Thanks to a bit of tinkering, this solution seems elegant and understandable now but as usual with generics it'll be mostly incomprehensible tomorrow :).
Links
No comments:
Post a Comment