@Builder API design.

85 views
Skip to first unread message

Reinier Zwitserloot

unread,
May 9, 2013, 8:47:35 PM5/9/13
to project...@googlegroups.com
How's this sound? Weird / open issues are in all caps so you can quickly spot the hard decisions we have to make here.


@Builder: Annotate any class 'XType' with this (error if on interface, enum).

Optional parameters: "via", which can specify a static method in XType that is called. If via is not there, the constructor is called.

_EVERY FIELD_ is included, even weird ones like fields with a $ in it. There is no way to specify alternate signatures (your only option is to make a private static method that takes each field in order and calls the 'right' real constructing method/constructor). This does mean it won't actually work with @AllArgsConstructor if you have fields that start with a $ or other weirdness. Static fields are of course skipped. We won't check the signature of the constructor / static method. We just call it, and if it doesn't exist, ecj/javac will emit the error, not us.

Optional parameter 2: "rebuilder=boolean". False by default. If true, an instance 'builder' method is generated which returns a builder that is initialized to all values of the instance. This method is called 'rebuilder' and the name cannot be changed (possible alternate plan is @Builder(rebuilder="myName"), but if we go that route that means you HAVE to specify some name, and the only way we can try and create some sort of convention here is if (rebuilder="") means it'll be called 'rebuilder'. That's why I prefer the boolean, and then we can all have a nice session of paint-the-bikeshed as to what it'll be called.


The builder itself will be an inner static class named XTypeBuilder. It's okay if this inner static class is already present; in that case, we'll inject all the methods we would have generated in there, and simply skip whatever already exists (this means you get to override whatever you want and add whatever you want to the builder).

inside XTypeBuilder, you'll find 1 method for each field. The method's name is equal to the field's name, it has 1 argument (equal to the field's type), and returns XTypeBuilder. There's also a build() method that returns XType and just calls the constructor which each field in order. This method never itself errors (so there is no intrinsic handling of mandatory fields. You'll have to error out in your constructor. IT WILL NOT BE POSSIBLE TO TELL THE DIFFERENCE between a build session where some field wasn't initialized at all, and a build session where that field was initialized to null / false / 0 / 0.0 / '\0'.

XTypeBuilder will also get a sane toString. __NO SANE EQUALS AND HASHCODE WILL BE GENERATED__. Builders are not considered inherently comparable like that. You are supposed to use them to make an instance and then get rid of em. It's somewhat suspect to start passing a builder around, and it would be completely crazy to try and shove them into lists and sets and the like, so let's not even try to go there. These are not intended as mutable standins for XType. If you must have it, you can always write @EqualsAndHashCode public static class XTypeBuilder {}. We'll make sure that that'll work.

THERE IS NO WAY TO CHANGE THE NAME of build() or builder(). We can always add this later, and I really would like to create a convention here. guava has already set the tone on builder().foo().bar().build().

NO SUPERFLUENCY. (superfluency is where you have a huge list of types, and each type has only 1 build method and returns the next type in the chain. This way, you just ctrl+space (autocomplete) your way through the entire deal, and there won't even be a build() method to call unless you've actually initialized each mandatory value. It's what lombok-pg's builder does. It's a neat idea but way too much typespace clutter and it looks ridiculous if delomboked).


example:

@Builder(via="_build", rebuilder=true) @Value
public class Person {
    String firstName, lastName;

    private static Person _build(String a, String b) {
        return (a.equals("Bruce") && b.equals("Wayne")) : Superheroes.SUPERMAN : new Person(a, b);
    }
}

results in all the usual things @Value would do, and:

public class Person {
    private final String firstName, lastName;

    public static PersonBuilder builder() { return new PersonBuilder(); }

    public PersonBuilder rebuilder() {
        return builder().firstName(firstName).lastName(lastName);
    }
    

    public static class PersonBuilder {
        private String firstName, lastName;

        PersonBuilder() {}

        public PersonBuilder firstName(String firstName) { this.firstName = firstName; }

        public PersonBuilder lastName(String firstName) { this.lastName = lastName; }

        public Person build() {
            return Person._build(firstName, lastName);
        }

        @java.lang.Override public java.lang.String toString() {
            return "PersonBuilder(firstName = " + firstName + ", lastName = " + lastName + ")";
        }
    }
}



I have no idea if it is possible to generate new inner classes in both ecj and javac (this isn't something we have ever done before), but lombok-pg does it so presumably it's possible.

eric.giese

unread,
May 11, 2013, 1:28:09 AM5/11/13
to project...@googlegroups.com
Sounds good and reasonable!


Am Freitag, 10. Mai 2013 02:47:35 UTC+2 schrieb Reinier Zwitserloot:
How's this sound? Weird / open issues are in all caps so you can quickly spot the hard decisions we have to make here.


@Builder: Annotate any class 'XType' with this (error if on interface, enum).

Optional parameters: "via", which can specify a static method in XType that is called. If via is not there, the constructor is called.

_EVERY FIELD_ is included, even weird ones like fields with a $ in it. There is no way to specify alternate signatures (your only option is to make a private static method that takes each field in order and calls the 'right' real constructing method/constructor). This does mean it won't actually work with @AllArgsConstructor if you have fields that start with a $ or other weirdness. Static fields are of course skipped. We won't check the signature of the constructor / static method. We just call it, and if it doesn't exist, ecj/javac will emit the error, not us.


I don't really get why @AllArgsConstructor and @Builder should behave differently here.
Obviously it is bogus to manually write fields starting with $, so this is a more of an edge case,
 
Optional parameter 2: "rebuilder=boolean". False by default. If true, an instance 'builder' method is generated which returns a builder that is initialized to all values of the instance. This method is called 'rebuilder' and the name cannot be changed (possible alternate plan is @Builder(rebuilder="myName"), but if we go that route that means you HAVE to specify some name, and the only way we can try and create some sort of convention here is if (rebuilder="") means it'll be called 'rebuilder'. That's why I prefer the boolean, and then we can all have a nice session of paint-the-bikeshed as to what it'll be called.

That way, @Builder becomes a rival for the @Wither-Feature.
Question is: Is this reasonable?
In theory, a builder will be faster on multiple values while @Wither is faster on single values.

However,
new XType().withFirst().withSecond()
reads more natural than
new XType().rebuilder().first().second().build() 

I think its OK that way.
 
The builder itself will be an inner static class named XTypeBuilder. It's okay if this inner static class is already present; in that case, we'll inject all the methods we would have generated in there, and simply skip whatever already exists (this means you get to override whatever you want and add whatever you want to the builder).

inside XTypeBuilder, you'll find 1 method for each field. The method's name is equal to the field's name, it has 1 argument (equal to the field's type), and returns XTypeBuilder. There's also a build() method that returns XType and just calls the constructor which each field in order. This method never itself errors (so there is no intrinsic handling of mandatory fields. You'll have to error out in your constructor. IT WILL NOT BE POSSIBLE TO TELL THE DIFFERENCE between a build session where some field wasn't initialized at all, and a build session where that field was initialized to null / false / 0 / 0.0 / '\0'.

Agreed. Builders should never allow it to inspect their values.
 

XTypeBuilder will also get a sane toString. __NO SANE EQUALS AND HASHCODE WILL BE GENERATED__. Builders are not considered inherently comparable like that. You are supposed to use them to make an instance and then get rid of em. It's somewhat suspect to start passing a builder around, and it would be completely crazy to try and shove them into lists and sets and the like, so let's not even try to go there. These are not intended as mutable standins for XType. If you must have it, you can always write @EqualsAndHashCode public static class XTypeBuilder {}. We'll make sure that that'll work.


Agreed. In pratice, no one implements hashcode and equals on builder.
It doesn't make sense to pass them around, it might even become dangerous if they are put in a Set or Map.

THERE IS NO WAY TO CHANGE THE NAME of build() or builder(). We can always add this later, and I really would like to create a convention here. guava has already set the tone on builder().foo().bar().build().

Agreed, custom naming for these isn't a good idea.
I'm not entirely convinced with 'rebuilder()' but I don' have a better idea.


NO SUPERFLUENCY. (superfluency is where you have a huge list of types, and each type has only 1 build method and returns the next type in the chain. This way, you just ctrl+space (autocomplete) your way through the entire deal, and there won't even be a build() method to call unless you've actually initialized each mandatory value. It's what lombok-pg's builder does. It's a neat idea but way too much typespace clutter and it looks ridiculous if delomboked).

Agreed. @Builder should be a replacement for manual writeable code, and the one-type-per-param implementation isn't.
 

Fabrizio Giudici

unread,
May 11, 2013, 2:46:43 AM5/11/13
to project...@googlegroups.com, Reinier Zwitserloot
On Fri, 10 May 2013 02:47:35 +0200, Reinier Zwitserloot
<rein...@gmail.com> wrote:

> The builder itself will be an inner static class named XTypeBuilder. It's
> okay if this inner static class is already present; in that case, we'll
> inject all the methods we would have generated in there, and simply skip
> whatever already exists (this means you get to override whatever you want
> and add whatever you want to the builder).
>
> inside XTypeBuilder, you'll find 1 method for each field. The method's
> name
> is equal to the field's name, it has 1 argument (equal to the field's
> type), and returns XTypeBuilder. There's also a build() method that
> returns
> XType and just calls the constructor which each field in order. This
> method
> never itself errors (so there is no intrinsic handling of mandatory
> fields.
> You'll have to error out in your constructor. IT WILL NOT BE POSSIBLE TO
> TELL THE DIFFERENCE between a build session where some field wasn't
> initialized at all, and a build session where that field was initialized
> to
> null / false / 0 / 0.0 / '\0'.

I'd like to see a configurable pattern for the methods. For instance, I
might want to use withField(5) rather than field(5).
> XTypeBuilder will also get a sane toString. __NO SANE EQUALS AND HASHCODE
> WILL BE GENERATED__. Builders are not considered inherently comparable
> like
> that. You are supposed to use them to make an instance and then get rid
> of
> em. It's somewhat suspect to start passing a builder around, and it would
> be completely crazy to try and shove them into lists and sets and the
> like,
> so let's not even try to go there. These are not intended as mutable
> standins for XType. If you must have it, you can always write
> @EqualsAndHashCode public static class XTypeBuilder {}. We'll make sure
> that that'll work.

Ok. Because I see some reasons for passing builder around, and even
persist them, as a variation of the "Prototype" pattern. E.g. you first
define the way you want create an object, and *later*, maybe in different
sessions, instantiate many objects like that. I'd see a builder that I
first create, even make persistent, and later retrieve and call build() on
it. Thus, I'd like to have the serializableUID problem addressed in some
way.

> THERE IS NO WAY TO CHANGE THE NAME of build() or builder(). We can always
> add this later, and I really would like to create a convention here.
> guava
> has already set the tone on builder().foo().bar().build().


Reasonable. The only minor detail I don't like is that underscore in
_build() :o) Perhaps we can just figure out a good name for the method.

--
Fabrizio Giudici - Java Architect @ Tidalwave s.a.s.
"We make Java work. Everywhere."
http://tidalwave.it/fabrizio/blog - fabrizio...@tidalwave.it
Reply all
Reply to author
Forward
0 new messages