record-builder icon indicating copy to clipboard operation
record-builder copied to clipboard

Make builder constructor public

Open toolforger opened this issue 2 years ago • 4 comments

The purpose is to make record-builder fit for a slightly off-label use case: Generate the getter/setter/equals/hashcode/toString boilerplate for our otherwise standard Java Bean classes. We don't oppose records, actually, it's just that some libraries don't fully support using them yet.

We would like to be able to make a bean class Foo like this:

public class Bean extends FooBaseRecordBuilder {
    // We need a parameterless constructor for a Java Bean.
    // If the parent class has it but makes it private, we can't even create it here.

    // Any overrides for getters/setters go here.
    // (If there are no overrides, maybe we can configure record-builder to create Foo directly.
    // We did not pursue that venue since our team is okay with having an empty Foo class.)
}

where FooBaseRecordBuilder is generated from a FooBase interface:

@RecordInterface
// Actually this is in a @RecordBuilder.Template :-)
@RecordBuilder.Options(booleanPrefix = "is", getterPrefix = "get", setterPrefix = "set")
public interface FooBase {

    boolean isCustomizable();

    @JsonView(Patchable.class) // Probably need a @RecordBuilder.Options entry to have this on the actual Java Bean
    boolean isEnabled();
}

Aside notes:

  • We do not need the generated FooRecord class.
  • In the FooRecordBuilder class, we need only the default constructor, the fields, getters/setters, and equals/hashcode/toString.
  • The stream is a very, very appreciated addition that will help us with some other stuff. We interact with MongoDB and Jackson, and being able to iterate over whatever fields are in an entity object is a perfect match for these. (It would be nice to have a stream that returns the getters and setters, but we have to encounter that use case yet.)

toolforger avatar Sep 15 '22 14:09 toolforger

Let's let this sit for a bit to see if others have feedback. Regardless, this should have an option to enable it. It should not be the default.

Randgalt avatar Sep 16 '22 09:09 Randgalt

Let's let this sit for a bit to see if others have feedback.

Agreed.

Regardless, this should have an option to enable it. It should not be the default.

I was considering it. Thought it's adding to the burden of things one has to configure, so I looked whether it's necessary.

Things that speak against making it configurable:

  • No existing code will be affected.
  • There's no need to prevent subclassing, as the Builder is mutable (by design), and I have yet to see another reason than immutability to prevent subclassing.

Things that speak for making it configurable:

  • If somebody comes up with a reason for making it un-subclassable by default, changing the default would be incompatible.

My personal take is that we should see if somebody comes up with a guarantee that an unsubclassable builder can give that a subclassable does not. That said, if you think a configuration option is the way to go, I prefer a fast decision even more and will happy add the option.

toolforger avatar Sep 16 '22 12:09 toolforger

That said, if you think a configuration option is the way to go, I prefer a fast decision even more and will happy add the option.

Yes, please.

Randgalt avatar Sep 17 '22 17:09 Randgalt

I see that to allow a record builder to be both subclassable and fluent, it needs an additional type parameter for the setters' return type.

This has ramifications for type parameters on Withers, factory methods, and such, but I don't have a useful overview. Should I just add the new type parameter to InternalRecordBuilderProcessor.typeVariables and "it will just work out", or do you expect problems? What generated files should I look at to identify problems? (I suppose somewhere in record-builder-test/target/generated-sources, but which ones?)

toolforger avatar Sep 18 '22 07:09 toolforger

I got into a situation when I need a public constructor. I'm working with AWS libraries which are written with Lombok-style generated builders in mind which have a public constructor in the builder and initialize builders via the said constructor. Pretty sure there are other libraries that are tailored towards Lombok-style builders. It would be nice to have the option of making the default, no-arg constructor, public.

yurybubnov avatar Mar 21 '23 06:03 yurybubnov

@yurybubnov when I get time I can enhance this to make it optional, etc. Or maybe you can do it if you can.

Randgalt avatar Mar 23 '23 12:03 Randgalt

This seems to be hanging around for a while. I'm moving from Immutables.io to this as I need to do some complex Jackson serialisation. In that framework i used to augment the builder with extra helpers, e.g:

public record Grouping(
    UUID id,
    String name,
    String description,
    String context,
    List<String> objectRefs )  {

    public static class Builder extends GroupingBuilder {
        public Builder id(String id) {
            this.id(UUID.fromString(id));
            return this;
        }
    }

    public static Builder build() {
        return new Builder();
    }

}

Just having both constructors on the builder as public or package would mean that this PR solves this use case. Another option for this would be to have a option to make the static builder() and builder(....) methods be package private. This would mean the only method would be the new one in the record itself.

This would mean the builder is "hidden". The immutables.io library does this.

KangoV avatar Nov 15 '23 14:11 KangoV

tbh - I'm not sure what to do about this. A few bad commits got into RecordBuilder. Thankfully, they're behind options but they are there nonetheless. There's a new PR I haven't looked at. I'm not sure if it addresses this.

Randgalt avatar Nov 15 '23 14:11 Randgalt

I do think that this should be behind an option, builderConstructorVisibility maybe? The default being private. This would not break existing code.

KangoV avatar Nov 15 '23 14:11 KangoV

@KangoV can you make a new PR with it behind an option?

Randgalt avatar Nov 15 '23 14:11 Randgalt

I'll have a go later and see how I get on.

KangoV avatar Nov 15 '23 14:11 KangoV

Anybody please feel free to take over and do as you wish. The project I needed this for and me have parted ways, meaning my feedback wouldn't be very useful anyway. (Also, not getting answers to my latest questions meant I wouldn't be triggered to continue work on this, so that's why there wasn't any progress. No offense taken though.)

toolforger avatar Nov 15 '23 15:11 toolforger

(Also, not getting answers to my latest questions meant I wouldn't be triggered to continue work on this, so that's why there wasn't any progress. No offense taken though.)

Please accept my apologies for lack of response. The last year or so has left me little time for side projects. I'm trying now to back to this.

Randgalt avatar Nov 15 '23 17:11 Randgalt

No need to apologize, I know such things happen :-) I just meant to give a heads-up that everybody please feel free to continue as is best for them, and an explanation that it's not anybody's fault.

toolforger avatar Nov 15 '23 17:11 toolforger

Should the default be public? I've had a look through the immutables.io project which I have been using and there is no option to make it private. Do you think an option is needed. I leaning towards not having one, unless someone shouts.

KangoV avatar Nov 25 '23 01:11 KangoV

It should be public if both apply:

  • There is no expectation that the API changes as the library evolves (in this case, that it doesn't change unless the data that goes into it generation changes).
  • It is useful for the users of the library, or enables library usage for new cases.

I believe both are true.

You may be reluctant to enable new use cases, since more varied use cases means more design constraints which means more design work and possibly more difficult design trade-offs.

I don't see big trouble ahead in this case, so I'd do it if it were my project. On the other hand I don't see all use cases already in existence, and how narrowed-down the design space already is, so my instincts may be working with incomplete data.

toolforger avatar Nov 25 '23 08:11 toolforger

I'd still like to be controlled by an option. This ship has sailed and I don't want to disrupt any existing use-cases.

Randgalt avatar Nov 25 '23 10:11 Randgalt

Not wanting to argue that the decision is already made... but making a constructor public isn't going to break any existing code, is it?

toolforger avatar Nov 25 '23 11:11 toolforger

You'd be surprised. This is more of a gut reaction based on experience. I've made what I thought were innocuous changes in this and other libraries only to be surprised by the reaction of the community.

Randgalt avatar Nov 25 '23 11:11 Randgalt

Replaced by https://github.com/Randgalt/record-builder/pull/160

Randgalt avatar Dec 19 '23 20:12 Randgalt