Skip to content

FEATURE IDEA: "Mandatory" fields with @Builder

Reinier Zwitserloot edited this page Oct 15, 2018 · 1 revision

We get this feature request every other week:

A way to have @Builder generate code such that things that are mandatory to set cause compile-time errors if you forget to set them.

There are two major ideas on how to implement this.

The builder() static method take each mandatory argument as parameter

This is workable enough for a single parameter, but the point of builder is nice, fluent API: It's the java way to have named parameters. This idea means we're right back to a mess o' parameters! We don't want to add this feature in this fashion.

Create a series of interfaces for each step

This idea centers around a convoluted stack of interfaces, each interface representing a step, and then having the builder class implement them all, with each step returning the appropriate interface. A pseudocode example:

@Builder class Example {
    @Mandatory int a, b;
    String extra;
}

generates:

public static ExampleBuilderStep1 builder() {
    return new ExampleBuilder();
}
interface ExampleBuilderStep1 {
    ExampleBuilderStep2 a(int a);
}
interface ExampleBuilderStep2 {
    ExampleBuilder b(int b);
}
class ExampleBuilder implements ExampleBuilderStep1, ExampleBuilderStep2 {
    private int a, b;
    private String extra;

    public ExampleBuilderStep2 a(int a) { this.a = a; return this; }
    public ExampleBuilder b(int b) { this.b = b; return this; }
    public Example build() { ... }
}

It's a tonne of boilerplate but the argument would be: Hey, lombok is involved; it's not like you can see it.

However, this is really missing the forest for the trees: You get your 'mandatory' builder, at the cost of a ridiculous mess. The cure is way, way worse than the disease. There are tons of problems with the above:

  • It becomes impossible to smear the job of building out across various modules (if this method sets one thing, and that method sets another...)
  • You are either forced to first set a, and then b, and then the rest, in that exact order, or you get a combinatorial explosion of interfaces.
  • Your autocomplete dialogs and outline views in your IDE get infected with a massive list of weird stuff.
  • You also need to consider the notion of having a builder where you override (set 'again') a value, and the notion of multi-settable things (i.e. it must be set at least once but can be set multiple times, for example with @Singular).

We veto this idea entirely.

That's because there is a VASTLY superior solution to this problem! Knowing there's something better out there means we have lost all interest in trying to solve this the lombok way.

The right way to do it

Write an IDE plugin that understands the notion of builders, which method creates a builder, which method finishes a build, and the 'set' methods of the builder; have annotations to mark both 'mandatory' and 'multiple' and have this plugin understand these annotations. Then, the plugin does the following:

IF the builder is not created in this method, but is provided to you via parameter or field, do nothing: We're not in a context where it is possible to fully analyse what you have and have not called.

Otherwise, analyse every call to the builder, starting at its instantiation (via the builder() method invocation): Upon opening the autocomplete dialog, grey out or even entirely remove all the j.l.Object methods because they aren't relevant, and grey out all 'invalid' methods. Invalid methods are any non-multiple methods that you've already called, and, as long as there is at least one mandatory method you haven't yet called, the build method is also 'invalid'. Actually invoking an 'invalid' method is still a legal move but the autocomplete dialog guides you by rendering these in grey.

Render 'must-call' methods in bold. Must-calls are all mandatory methods which are not yet called. One could consider the 'build' method itself a must-call once all mandatories are set.

That leaves the 'ambivalance' methods, which are rendered plain (neither greyed out nor highlighted): All @Multiple @Mandatory methods that have already been called at least once, all optional (not @Mandatory) methods that haven't yet been called, and all @Multiple methods whether they have been called yet or not.

In addition, render the entire invocation chain with a warning mark (in most IDEs, that'd be a yellow wavy underline), with the warning message stating in plain english what the problem is: For something like SomeType.builder().foo(1).build(), if there is a mandatory method named bar(), the error message is on the build() node and reads: attempt to build without invoking mandatory method "bar". - and you can also consider marking the entire node as a warning if you do nothing with it and forget to invoke build(); builders generally are a no-op if you don't use the result, thus, lack of usage warrants generating a warning.

Lombok should not provide this plugin; it's just not what lombok is. That'd be a different project. An excellent take on this idea would include plugins for eclipse and intellij, and a command line tool that can lint all files thrown at it, with documentation on how to use it as a commit hook.

If such a project is ever made, we will cheerlead it. Lombok will add and support whatever annotations you need to make it work.

But lombok will not create an explosion of WTF code to make a weak half-baked take on this plugin.