projectlombok / lombok

Very spicy additions to the Java programming language.
https://projectlombok.org/
Other
12.94k stars 2.4k forks source link

Similar to toBuilder=true, add the ability to have a newBuilder instance method generated via @Builder(newBuilder=true) so buildable objects can be treated generically #958

Closed jmax01 closed 9 years ago

jmax01 commented 9 years ago

Similar to toBuilder=true, add the ability to have a B newBuilder() instance method generated via @Builder(newBuilder=true) so buildable objects can be treated generically.

A great thing about @Builder is that by declaring an empty *Builder class one can have the generated builder implement interfaces:

    public static interface WithName {

        String getName();

        public static interface WithNameSetter<BUILDER extends WithName.WithNameSetter<? super BUILDER>> {

            BUILDER name(String name);
        }
    }

    @Value
    @Builder
    public static class MyClass implements WithName {

        final private String name;

        public static class MyClassBuilder implements WithName.WithNameSetter<MyClass.MyClassBuilder> {}
    }

    public static final <B extends WithName.WithNameSetter<B>> B populateName(final B builder, final String newName) {
        return builder.name(newName);
    }

    @Test
    public void testPopulateName() {

        final String expected = "Alice";

        final MyClass.MyClassBuilder resultBuilder = NewBuilderSimpleTest.populateName(MyClass.builder(), expected);

        final WithName result = resultBuilder.build();

        assertEquals(expected, result.getName());
    }

Suppose we want to be able to change a value in a generic fashion and instead of returning the builder we return the built object?

We simply add a WithBuild<T> interface to our builder and make sure the builder type (B) in populateName(B builder) implements WithBuild<T>:

    public interface WithBuild<T> {

        T build();
    }

    public static interface WithName {

        String getName();

        public static interface WithNameSetter<BUILDER extends WithName.WithNameSetter<? super BUILDER>> {

            BUILDER name(String name);
        }
    }

    @Value
    @Builder(toBuilder = true)
    public static class MyClass implements WithName {

        final private String name;

        public static class MyClassBuilder implements WithBuild<MyClass>,
                WithName.WithNameSetter<MyClass.MyClassBuilder> {}
    }

    public static final <B extends WithName.WithNameSetter<B> & WithBuild<T>, T> T populateName(final B builder,
            final String newName) {

        return builder.name(newName)
            .build();
    }

    @Test
    public void testPopulateName() {

        final String expected = "Alice";

        final MyClass result = populateName(MyClass.builder(), expected);

        assertEquals(expected, result.getName());
    }

Now suppose we want to create new objects with different values from existing objects.

We need to add toBuilder=true to our @Builder annotation and introduce the interface WithBuilder<T extends WithBuilder<T,B>,B extends WithBuild<T>> that has a B toBuilder() method.

We also add a new field type as B toBuilder() on a single valued type is not very valuable.

It is important to note that aside from implementing the WithBuilder interface we do not have to modify the class:

    public interface WithBuild<T> {

        T build();
    }

    public interface WithBuilder<T extends WithBuilder<T, B>, B extends WithBuild<T>> {

        B toBuilder();
    }

    public static interface WithType {

        String getType();

        public static interface WithTypeSetter<B extends WithType.WithTypeSetter<? super B>> {

            B type(String name);
        }
    }

    public static interface WithName {

        String getName();

        public static interface WithNameSetter<B extends WithName.WithNameSetter<? super B>> {

            B name(String name);
        }
    }

    @Value
    @Builder(toBuilder = true)
    public static class MyClass implements WithName, WithType, WithBuilder<MyClass, MyClass.MyClassBuilder> {

        final private String name;

        final private String type;

        public static class MyClassBuilder implements WithBuild<MyClass>,
                WithName.WithNameSetter<MyClass.MyClassBuilder>, WithType.WithTypeSetter<MyClass.MyClassBuilder> {}
    }

    public static final <B extends WithName.WithNameSetter<B> & WithType.WithTypeSetter<B> & WithBuild<T>, T extends WithBuilder<T, B>> T changeName(
            T row, final String newName) {

        return row.toBuilder()
            .name(newName)
            .build();
    }

    @Test
    public void testChangeName() {

        final String initialName = "Alice";

        final String initialType = "A";

        MyClass initial = MyClass.builder()
            .name(initialName)
            .type(initialType)
            .build();

        final String expectedName = "Bob";

        final MyClass result = changeName(initial, expectedName);

        assertEquals(expectedName, result.getName());

        assertEquals(initialType, result.getType());
    }

Now suppose we want to change a subset of fields in some cases and change them all in other cases?

In that case we need both a B toBuilder() and B newBuilder() method on the WithBuilder interface.

However since B builder() is static and not an instance method it is not visible generically and thus we are required to implement a B newBuilder() instance method on every object that implements WithBuilder.

Automatically adding a B newBuilder() instance method would eliminate the need for this boilerplate:

    public interface WithBuild<T> {

        T build();
    }

    public interface WithBuilder<T extends WithBuilder<T, B>, B extends WithBuild<T>> {

        B toBuilder();

        B newBuilder();
    }

    public static interface WithType {

        String getType();

        public static interface WithTypeSetter<B extends WithType.WithTypeSetter<? super B>> {

            B type(String name);
        }
    }

    public static interface WithName {

        String getName();

        public static interface WithNameSetter<B extends WithName.WithNameSetter<? super B>> {

            B name(String name);
        }
    }

    @Value
    @Builder(toBuilder = true)
    public static class MyClass implements WithName, WithType, WithBuilder<MyClass, MyClass.MyClassBuilder> {

        final private String name;

        final private String type;

        // This should be handled by @Builder(toBuilder = true, newBuilder = true)
        @Override
        public MyClassBuilder newBuilder() {

            return MyClass.builder();
        }

        public static class MyClassBuilder implements WithBuild<MyClass>,
                WithName.WithNameSetter<MyClass.MyClassBuilder>, WithType.WithTypeSetter<MyClass.MyClassBuilder> {}

    }

    public static final <B extends WithName.WithNameSetter<B> & WithType.WithTypeSetter<B> & WithBuild<T>, T extends WithBuilder<T, B> & WithName & WithType> T changeFieldsConditionally(
            T row, final String newName) {

        if ("C".equals(row.getType())) {

            return row.newBuilder()
                .name(row.getName()
                    .toUpperCase())
                .type("A")
                .build();
        }

        return row.toBuilder()
            .name(newName)
            .build();
    }

    @Test
    public void testChangeFieldsConditionally() {

        final String initialAliceName = "Alice";

        final String initialAliceType = "C";

        MyClass initialAlice = MyClass.builder()
            .name(initialAliceName)
            .type(initialAliceType)
            .build();

        final String expectedAliceName = "ALICE";

        final String expectedAliceType = "A";

        final String expectedName = "Bruce";

        final MyClass aliceResult = changeFieldsConditionally(initialAlice, expectedName);
        assertEquals(expectedAliceName, aliceResult.getName());
        assertEquals(expectedAliceType, aliceResult.getType());

        final String initialBobName = "Bob";

        final String initialBobType = "B";
        final String expectedBobType = initialBobType;

        MyClass initialBob = MyClass.builder()
            .name(initialBobName)
            .type(initialBobType)
            .build();

        final MyClass bobResult = changeFieldsConditionally(initialBob, expectedName);

        assertEquals(expectedName, bobResult.getName());
        assertEquals(expectedBobType, bobResult.getType());
    }
rzwitserloot commented 9 years ago

This is exotic. Which means lombok won't do it; lombok does boilerplate, and boilerplate is defined as non-exotic.

We always have to weigh exoticness against difficulty of handwriting it. Fortunately, here the call is easy: Adding a 'newBuilder' instance method is easy to do and doesn't require you to modify it when you add or remove fields (you'd have to modify it when you add or remove generics params but that happens far less often).