I'm aware there've been similar questions. I haven't seen an answer to my question though.
I'll present what I want with some simplified code. Say I have a complex object, some of its values are generic:
public static class SomeObject<T, S> {
public int number;
public T singleGeneric;
public List<S> listGeneric;
public SomeObject(int number, T singleGeneric, List<S> listGeneric) {
this.number = number;
this.singleGeneric = singleGeneric;
this.listGeneric = listGeneric;
}
}
I'd like to construct it with fluent Builder syntax. I'd like to make it elegant though. I wish it worked like that:
SomeObject<String, Integer> works = new Builder() // not generic yet!
.withNumber(4)
// and only here we get "lifted";
// since now it's set on the Integer type for the list
.withList(new ArrayList<Integer>())
// and the decision to go with String type for the single value
// is made here:
.withTyped("something")
// we've gathered all the type info along the way
.create();
No unsafe cast warnings, and no need to specify generic types upfront (at the top, where Builder is constructed).
Instead, we let the type info to flow in explicitly, further down the chain - along with withList
and withTyped
calls.
Now, what would be the most elegant way to achieve it?
I'm aware of the most common tricks, such as the use of recursive generics, but I toyed with it for a while and couldn't figure out how it applies to this use case.
Below is a mundane verbose solution which works in the sense of satisfying all requirements, but at the cost of great verbosity - it introduces four builders (unrelated in terms of inheritance), representing four possible combinations of T
and S
types being defined or not.
It does work, but that's hardly a version to be proud of, and unmaintainable if we expected more generic parameters than just two.
public static class Builder {
private int number;
public Builder withNumber(int number) {
this.number = number;
return this;
}
public <T> TypedBuilder<T> withTyped(T t) {
return new TypedBuilder<T>()
.withNumber(this.number)
.withTyped(t);
}
public <S> TypedListBuilder<S> withList(List<S> list) {
return new TypedListBuilder<S>()
.withNumber(number)
.withList(list);
}
}
public static class TypedListBuilder<S> {
private int number;
private List<S> list;
public TypedListBuilder<S> withList(List<S> list) {
this.list = list;
return this;
}
public <T> TypedBothBuilder<T, S> withTyped(T t) {
return new TypedBothBuilder<T, S>()
.withList(list)
.withNumber(number)
.withTyped(t);
}
public TypedListBuilder<S> withNumber(int number) {
this.number = number;
return this;
}
}
public static class TypedBothBuilder<T, S> {
private int number;
private List<S> list;
private T typed;
public TypedBothBuilder<T, S> withList(List<S> list) {
this.list = list;
return this;
}
public TypedBothBuilder<T, S> withTyped(T t) {
this.typed = t;
return this;
}
public TypedBothBuilder<T, S> withNumber(int number) {
this.number = number;
return this;
}
public SomeObject<T, S> create() {
return new SomeObject<>(number, typed, list);
}
}
public static class TypedBuilder<T> {
private int number;
private T typed;
private Builder builder = new Builder();
public TypedBuilder<T> withNumber(int value) {
this.number = value;
return this;
}
public TypedBuilder<T> withTyped(T t) {
typed = t;
return this;
}
public <S> TypedBothBuilder<T, S> withList(List<S> list) {
return new TypedBothBuilder<T, S>()
.withNumber(number)
.withTyped(typed)
.withList(list);
}
}
Is there a more clever technique I could apply?
Okay, so the more traditional step-builder approach would be something like this.
Unfortunately, because we're mixing generic and non-generic methods, we have to redeclare a lot of methods. I don't think there's a nice way around this.
The basic idea is just: define each step on an interface, then implement them all on the private class. We can do that with generic interfaces by inheriting from their raw types. It's ugly, but it works.
Since we don't want people accessing the ugly implementation, we make it private and make everyone go through a
static
method.Otherwise it works pretty much the same as your own idea:
What is this about? Okay, so there's a problem in general here, and it's like this:
If we didn't create copies, we'd now have
123
masquerading around as aString
.(If you're only using the builder as the fluent set of calls, this can't happen.)
Although we don't need to make a copy for
withNumber
, I just went the extra step and made the builder immutable. We're creating more objects than we have to but there isn't really another good solution. If everyone is going to use the builder in the correct manner, then we could make it mutable andreturn this
.Since we're interested in novel generic solutions, here is a builder implementation in a single class.
The difference here is that we don't retain the types of
typed
andlist
if we invoke either of their setters a second time. This isn't really a drawback per se, it's just different I guess. It means that we can do this:Same thing about creating copies and heap pollution.
Now we're getting really novel. The idea here is that we can "disable" each method by causing a capture conversion error.
It's a little complicated to explain, but the basic idea is:
?
.The difference between this example and the previous example is that if we try to call a setter a second time, we will get a compiler error:
Thus, we can only call each setter once.
The two major downsides here are that you:
null
literal.I think it's a pretty interesting proof-of-concept, even if it's a little impractical.
Again, same thing about creating copies and heap pollution.
Anyway, I hope this gives you some ideas to sink your teeth in to. : )
If you're generally interested in this sort of thing, I recommend learning code generation with annotation processing, because you can generate things like this much easier than writing them by hand. As we talked about in the comments, writing things like this by hand becomes unrealistic pretty quickly.