2
\$\begingroup\$

I was thinking about how to minimize the code needed to implement a builder.

There is some code that any builder needs to have:

  • Temporary storage of property values
  • Validation of type
  • Null check
  • Mandatory properties check

So I defined a builder interface:

public interface GenericBuilder<Any> {
    Any build();
    GenericBuilder<Any> with(Property property, Object value);
}

I also have a class for individual properties:

public static class Property {
    public enum NullCheck {
        NULLABLE {
            @Override
            public void check(Object value, Class<?> clazz) {
              // do nothing
            }
        },
        NOT_NULL {
            @Override
            public void check(Object value, Class<?> clazz) {
                Objects.requireNonNull(value, String.format("Property of type %s must not be null", clazz));
            }
        };
        public abstract void check(Object value, Class<?> clazz);
    }

    private final Class<?> clazz;
    private final NullCheck nullCheck;

    public Property(Class<?> clazz) {
        this(clazz, NullCheck.NOT_NULL);
    }

    public Property(Class<?> clazz, NullCheck nullCheck) {
        this.clazz = clazz;
        this.nullCheck = nullCheck;
    }

    @SuppressWarnings("unchecked")
    public <T> T getFrom(Map<GenericBuilder.Property, ?> propertyMap) {
        return (T) propertyMap.get(this);
    }

    public void verifyType(Object value) {
        if (!clazz.isInstance(value)) {
            throw new ClassCastException(String.format("Cannot cast %s to %s", value.getClass(), clazz));
        }
        nullCheck.check(value, clazz);
    }
}

The common builder behavior is placed in an abstract builder implementation:

public static abstract class AbstractBuilder<Any> implements GenericBuilder<Any> {
    private final Map<Property, Object> properties = new HashMap<>();
    private final List<Property> mandatoryProperties;
    public AbstractBuilder(Property... mandatoryProperties) {
        this.mandatoryProperties = Arrays.asList(mandatoryProperties);
    }

    protected void validate() {
        ArrayList<Property> missingProperties = new ArrayList<>(mandatoryProperties);
        missingProperties.removeAll(properties.keySet());
        if (!missingProperties.isEmpty()) {
            throw new IllegalStateException(String.format("Mandatory Properties not given: %s", missingProperties));
        }
    }

    @SuppressWarnings("unchecked")
    protected <T> T get(Property property) {
        return (T) properties.get(property);
    }

    @Override
    public Any build() {
        validate();
        return buildDto();
    }

    @Override
    public GenericBuilder<Any>with( Property property, Object value) {
        if (properties.containsKey(property)) {
            throw new IllegalStateException(String.format("Property must not be given twice"));
        }
        property.verifyType(value);
        properties.put(property, value);
        return this;
    }

    protected abstract Any buildDto();
}

Here is how the builder is used, quite small and neat:

    static interface Dependency {}    
    static class DependencyImpl implements Dependency {}

    static class MyClass {
        // final properties here
        public MyClass(BigInteger object, String object2, Dependency dependency) {
            // assign parameters to properties
        }
    }

    static class MyBuilder extends AbstractBuilder<MyClass> {
        private static final PropertyImpl PROPERTY1
                = new PropertyImpl(Dependency.class);
        private static final PropertyImpl PROPERTY2
                = new PropertyImpl(String.class, NullCheck.NULLABLE);
        private static final PropertyImpl PROPERTY3
                = new PropertyImpl(BigInteger.class);

        public MyBuilder() {
            super(PROPERTY1);
        }

        @Override
        protected MyClass buildDto() {
            return new MyClass(get(PROPERTY1), get(PROPERTY2), get(PROPERTY3));
        }
    }

And of course I have a JUnit test:

public class DtoBuilderTest {
    @Rule
    public ExpectedException exception = ExpectedException.none();
    @SuppressWarnings("unused")
    private MyClass myObject;

    @Test
    public void withAllPropertiesDtoIsCreated() {
        myObject = new MyBuilder().with(MyBuilder.PROPERTY3, BigInteger.TEN)
                .with(MyBuilder.PROPERTY2, "fadgag")
                .with(MyBuilder.PROPERTY1, new DependencyImpl())
                .build();
    }

    @Test
    public void withMissingOptiopnalPropertiesDtoIsCreated() {
        GenericBuilder<MyClass> dtoBuilder = new MyBuilder().with(MyBuilder.PROPERTY1, new DependencyImpl());
        myObject = dtoBuilder.build();
    }

    @Test
    public void withMissingMantatoryPropertyExceptionIsTrownOnBuild() {
        GenericBuilder<MyClass> dtoBuilder = new MyBuilder().with(MyBuilder.PROPERTY2, "ydfbgadfsd");
        exception.expect(IllegalStateException.class);
        myObject = dtoBuilder.build();
    }

    @Test
    public void withWrongPropertyTypeExceptionIsTrownOnSet() {
        exception.expect(ClassCastException.class);
        new MyBuilder().with(MyBuilder.PROPERTY1, BigInteger.ZERO);
    }

    @Test
    public void withWrongPropertyTypeExceptionIsTrownOnSet2() {
        exception.expect(ClassCastException.class);
        new MyBuilder().with(MyBuilder.PROPERTY3, "ydfbgadfsd");
    }

    @Test
    public void withDoubledPropertyExceptionIsTrownOnSet() {
        exception.expect(IllegalStateException.class);
        new MyBuilder().with(MyBuilder.PROPERTY2, "ydfbgadfsd").with(MyBuilder.PROPERTY2, "ydfbgadfsd");
    }    
}

My concern is that the error messages are not specific enough. I don't like the idea to pass the property names as parameter to the Property constructor since this feels like unnecessary duplication.

I'd also like to remove the need for listing the properties as constants in the concrete builder at all. Ideally I'd like to get the properties via annotation from the target class, but I don't know how.

Until then, I'd really like the list of properties in the concrete builder to be an enum so that the constant name is available for the error message. But I also want to avoid to have to implement the public methods of Propertyin the enums in every new builder.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.