Well, thatโs why a more traditional approach to creating a step would be like this.
Unfortunately, since we mix general and non-general methods, we have to reuse many methods. I do not think there is a good way.
The basic idea is this: define each step on the interface, and then implement them all in a private class. We can do this using common interfaces, inheriting their original types. It is ugly, but it works.
public interface NumberStep { NumberStep withNumber(int number); } public interface NeitherDoneStep extends NumberStep { @Override NeitherDoneStep withNumber(int number); <T> TypeDoneStep<T> withTyped(T type); <S> ListDoneStep<S> withList(List<S> list); } public interface TypeDoneStep<T> extends NumberStep { @Override TypeDoneStep<T> withNumber(int number); TypeDoneStep<T> withTyped(T type); <S> BothDoneStep<T, S> withList(List<S> list); } public interface ListDoneStep<S> extends NumberStep { @Override ListDoneStep<S> withNumber(int number); <T> BothDoneStep<T, S> withTyped(T type); ListDoneStep<S> withList(List<S> list); } public interface BothDoneStep<T, S> extends NumberStep { @Override BothDoneStep<T, S> withNumber(int number); BothDoneStep<T, S> withTyped(T type); BothDoneStep<T, S> withList(List<S> list); SomeObject<T, S> create(); } @SuppressWarnings({"rawtypes","unchecked"}) private static final class BuilderImpl implements NeitherDoneStep, TypeDoneStep, ListDoneStep, BothDoneStep { private final int number; private final Object typed; private final List list; private BuilderImpl(int number, Object typed, List list) { this.number = number; this.typed = typed; this.list = list; } @Override public BuilderImpl withNumber(int number) { return new BuilderImpl(number, this.typed, this.list); } @Override public BuilderImpl withTyped(Object typed) {
Since we donโt want people to turn to an ugly implementation, we make it confidential and force everyone to go through the static method.
Otherwise, it works almost the same as your own idea:
SomeObject<String, Integer> works = SomeObject.builder() .withNumber(4) .withList(new ArrayList<Integer>()) .withTyped("something") .create();
// we could return 'this' at the risk of heap pollution
What is it? Ok, so here is the problem in general, and like this:
NeitherDoneStep step = SomeObject.builder(); BothDoneStep<String, Integer> both = step.withTyped("abc") .withList(Arrays.asList(123));
If we did not create copies, now we will have 123 masquerades around as String .
(If you use only the builder as a free set of calls, this cannot be.)
Although we do not need to make a copy for withNumber , I just took an extra step and made the builder unchanged. We create more objects than necessary, but in reality there is no other good solution. If everyone uses the constructor correctly, we can make it mutable and return this .
Since we are interested in new generic solutions, here is the implementation of the builder in one class.
The difference is that we do not save the types typed and list if we refer to one of their setters a second time. This is actually not the most disadvantage, it is just different, I think. This means that we can do this:
SomeObject<Long, String> = SomeObject.builder() .withType( new Integer(1) ) .withList( Arrays.asList("abc","def") ) .withType( new Long(1L) )
public static class OneBuilder<T, S> { private final int number; private final T typed; private final List<S> list; private OneBuilder(int number, T typed, List<S> list) { this.number = number; this.typed = typed; this.list = list; } public OneBuilder<T, S> withNumber(int number) { return new OneBuilder<T, S>(number, this.typed, this.list); } public <TR> OneBuilder<TR, S> withTyped(TR typed) {
The same goes for making copies and fouling heaps.
Now we are very romantic. The idea here is that we can โdisableโ each method, causing a capture conversion error.
A bit hard to explain, but the main idea:
- Each method somehow depends on a type variable declared in the class.
- "Disable" this method, specifying the type of the return value, enter a type variable
? . - This results in a capture conversion error if we try to call a method for this return value.
The difference between this example and the previous example is that if we try to call the setter a second time, we get a compiler error:
SomeObject<Long, String> = SomeObject.builder() .withType( new Integer(1) ) .withList( Arrays.asList("abc","def") ) .withType( new Long(1L) )
Thus, we can only call one setter once.
The two main drawbacks here are that you:
- cannot call setters a second time for legitimate reasons.
- and can call setters a second time with the
null literal.
I think this is a pretty interesting proof of concept, even if it's a little impractical.
public static class OneBuilder<T, S, TCAP, SCAP> { private final int number; private final T typed; private final List<S> list; private OneBuilder(int number, T typed, List<S> list) { this.number = number; this.typed = typed; this.list = list; } public OneBuilder<T, S, TCAP, SCAP> withNumber(int number) { return new OneBuilder<T, S, TCAP, SCAP>(number, this.typed, this.list); } public <TR extends TCAP> OneBuilder<TR, S, ?, SCAP> withTyped(TR typed) {
Again, about making copies and polluting the heap.
Anyway, I hope this gives you some ideas to drown your teeth. :)
If you are interested in such things at all, I recommend studying with annotation processing , because you can generate such things much easier than writing them manually. As we said in the comments, manually recording such things becomes unrealistic pretty quickly.