0

I annotated with @Singular to a Map, but I can't obtain the Map variable in the builder object.

My Class:

@Getter
@Builder
public class WebServiceStatus {
    private final String              wsdl;
    private final String              operation;
    @Singular
    private final Map<String, String> params;

    public WebServiceStatus(WebServiceStatusBuilder builder) {
        this.wsdl = builder.wsdl;
        this.operation = builder.operation;

        this.params = builder.params; // FAIL - Can't obtain params variable
    }
}

Lombok Generates the builder with this fields without Map type for params:

WebServiceStatusBuilder {
 wsdl: String
 operation: String
 params$key: ArrayList<String>
 params$value ArrayList<String> 
}

Extra Questions:

  • Do I have to create my own Getter Method for obtain the Map? (Any problem with this solution?)
  • Why lombok not generate or include an Getter for @Singular Maps?
fvpaz
  • 1
  • 2
  • 2
  • Accroding to documentation : 1) By annotating one of the parameters (if annotating a method or constructor with @Builder) or fields (if annotating a class with @Builder) with the @Singular annotation, lombok will treat that builder node as a collection, and it generates 2 'adder' methods instead of a 'setter' method. One which adds a single element to the collection, and one which adds all elements of another collection to the collection. No setter to just set the collection (replacing whatever was already added) will be generated. 2) You will need to make your own realisation of builder. – Vadim Yemelyanov Jul 27 '23 at 10:38
  • 2
    Why are you declaring a constructor that takes a Builder as a parameter? Lombok will generate a `build` method on the Builder, use that instead. – Jorn Jul 27 '23 at 10:40
  • @Jorn I Have a custom Builder for add extra methods (Not included in Post). In the Builder pattern, I like send The Builder to the Constructor because I reassure myself that the Builder is always used for create The Object. – fvpaz Jul 27 '23 at 10:57
  • All I can say to that is "don't do that". Maybe you can just make the actual constructor private? – Jorn Jul 27 '23 at 11:00
  • Thx. It's my first time with @Builder, Without Lombok I always build() the Object sending the Builder to Constructor following builder patterns tutorials. I have never had problems, what issues I could get. – fvpaz Jul 27 '23 at 11:51
  • No tutorial tells you to send the builder to a constructor. Which tutorial are you talking about? You just write a class with some fields and no constructors at all, slap `@Builder` on, and call `Person.builder().name("fvpaz").favouriteSite("StackOverflow").build();` and it all just works. – rzwitserloot Jul 27 '23 at 13:19
  • @rzwitserloot Yes i know it. I could not tell you than tutorial or post, It was 6 years ago . But I find this: ([Sample Tutorial](https://www.digitalocean.com/community/tutorials/builder-design-pattern-in-java)) – fvpaz Jul 27 '23 at 13:57
  • @fvpaz there are a few strategies on how to write builders; lombok chose for the more common model of having _the builder_ do the work of constructing, instead of having the constructor do it. – rzwitserloot Jul 27 '23 at 15:54

1 Answers1

0

Your question isn't entirely clear; but I think the answer to your question is very very simple.

The simple case

The only problem is what I call a 'doctor it hurts when I smash this hammer into my face' problem. You're making your own life hard for no reason and the solution is simply: Stop doing that.

Lombok makes all the infrastructure for you. There is no need to also make a constructor that takes the builder and then write by hand all the code to copy the values in the builder into your own fields.

Forget about lombok for a moment. Just think of the general principle of builders. There needs to be some method somewhere that does the job of actually 'constructing' - it will take all those non-final fields in the builder type and set the (usually) final fields in the actual class to these values.

There are 2 different takes on where this code should go. One option is that the main class has a 'simple' constructor (one that takes every field as parameter and just has a bunch of this.fieldName = fieldName; statements in it), and the builder class has a method that does the hard work. The second take is that the main class has only a constructor that accepts an instance of the builder and it does the work.

The first concept is slightly more popular than the second.

Lombok only supports the first. This isn't a problem: It's an implementation detail (that constructor, regardless of which 'take' on the idea you want, should be private, hence, it just doesn't matter; which choice you make is invisible to all code outside of that source file anyway).

Lombok already generates the second mode: There's a build() method in the generated builder class that does the work, there is no need to write a constructor that also does it. In fact, that's actively bad, because now you get endless, pointless style debates; should one call Person.builder().name("Jane").age(40).build(); or should one call new Person(Person.builder().name("Jane").age(40));? You shouldn't introduce 2 ways of doing a thing if there are no clear alternate use cases where one is better than the other for both alternatives.

Thus, the solution is very very simple: Just.. delete that constructor. Done. If you have new Person(Person.builder()...); anywhere in your code base, that'll now have an error. Fix it by simply removing the new Person() part and tossing a .build() at the end of the build chain. Easy.

You've removed a bunch of code from your source files and your API is now better. Double win!

The complex case

It's not at all in your snippets, but it's possible you really do need other internal code (as in, in the same source file, as the builder's stuff is all private) to have access to the map of a builder 'mid flight' (after being made but before being used to create an instance).

Lombok simply does not support this. The reasons for this are somewhat complex and mostly spelled out in the documentation. Oversimplifying a bit: Because it's more efficient this way.

Once you toss @Singular on a thing that you also want a @Builder for, lombok officially washes its hands: The code it generates is no longer designed to be 'directly interacted with / interoperated with' by the programmer at all. You let lombok do its thing, and if lombok's thing is no good, you don't use that feature of lombok. (see docs - or just take it from me, I'm a core contributor to the project).

Hence, we've ended up in a place you usually end up when you take lombok features and need it to go places that are fairly far away from the well-trodden path: Lombok is a tool to generate boilerplate. What you want is sufficiently uncommon that it has fully left the realm of 'boilerplate', and therefore, lombok's "mission statement" no longer applies to your case. If lombok can cater to your needs 'for free' (without complicating the tool for other users and without causing significant maintenance issues) we'd do that, but likely it can't - as it is here. Allowing the @Singular 'map' to be accessible mid-build has a cost, and any but the tiniest of costs is unacceptable given that lombok isn't meant to cater to your needs.

To be clear, I do not think this applies to your scenario (the first, simple case does - simply get rid of that constructor). But, if someone has a valid use case for accessing that map mid-build, the answer is: Get rid of @Builder and write it by hand; possibly use java -jar lombok.jar delombok to give you a basic skeleton to work off of.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72