In his excellent book "Effective Java (2nd edition)" Joshua Bloch describes a variation of the Builder design pattern for instantiating objects with multiple optional attributes.
Sticking to this pattern frees you from providing multiple constructors with the different optional attributes as parameters (hard to maintain and hard to read for clients) or providing setter methods for the optional attributes (require objects to be mutable, can leave objects in inconsistent state).
As Bloch points out, it's a very good idea to check any invariants applying to the object to be created within the builder's build() method. That way it is ensured, that clients can only retrieve valid object instances from the builder.
If you are using the Bean Validation API (JSR 303) to define constraints for your object model, this can be realized by validating these constraints within the build() method.
The following listing shows an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
public class Customer { private long id; private String firstName; private String lastName; private Date birthday; private Customer(Builder builder) { this.id = builder.id; this.firstName = builder.firstName; this.lastName = builder.lastName; this.birthday = builder.birthday; } public static class Builder { private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); private long id; private String firstName; private String lastName; private Date birthday; public Builder(long id, String lastName) { this.id = id; this.lastName = lastName; } public Builder firstName(String firstName) { this.firstName = firstName; return this; } public Builder birthday(Date birthday) { this.birthday = birthday; return this; } public Customer build() throws ConstraintViolationException { Customer customer = new Customer(this); Set<ConstraintViolation<Customer>> violations = validator.validate(customer); if (!violations.isEmpty()) { throw new ConstraintViolationException( new HashSet<ConstraintViolation<?>>(violations)); } return customer; } } @Min(1) public long getId() { return id; } @Size(min = 3, max = 80) public String getFirstName() { return firstName; } @Size(min = 3, max = 80) @NotNull public String getLastName() { return lastName; } @Past public Date getBirthday() { return birthday; } } |
The listing shows an exemplary model class Customer for which some invariants apply (e.g. a customer's last name must not be null and must be between 3 and 80 characters long). These invariants are expressed using constraint annotations from the Bean Validation API at the getter methods of the Customer class.
The inner class Builder is in charge of creating Customer instances. All mandatory fields either primitive (e.g. id) or annotated with @NotNull (e.g. lastName) are part of the builder's constructor. For all optional fields setter methods on the builder are provided.
Within the build() method the newly created Customer instance is validated using the Validator#validate() method. If any constraint violations occur, a ConstraintViolationException is thrown. That way it's impossible to retrieve an invalid Customer instance. The following unit test shows an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class CustomerTest { @Test public void validCustomer() { Customer c = new Customer.Builder(1, "Smith") .firstName("Bob") .birthday(new GregorianCalendar(1970, 3, 10).getTime()) .build(); assertNotNull(c); } @Test public void lastNameNullAndBirthdayInFuture() { try { new Customer.Builder(1, null) .birthday(new GregorianCalendar(2020, 3, 10).getTime()) .build(); fail("Expected ConstraintViolationException wasn't thrown."); } catch (ConstraintViolationException e) { assertEquals(2, e.getConstraintViolations().size()); } } } |
If there are multiple classes for which you want to provide a builder in the described way, it is useful to extract the validation routine into a base class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public abstract class AbstractBuilder<T> { private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); protected abstract T buildInternal(); public T build() throws ConstraintViolationException { T object = buildInternal(); Set<ConstraintViolation<T>> violations = validator.validate(object); if (!violations.isEmpty()) { throw new ConstraintViolationException( new HashSet<ConstraintViolation<?>>(violations)); } return object; } } |
Concrete builder classes have to extend AbstractBuilder and must implement the buildInternal() method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
... public static class Builder extends AbstractBuilder<Customer> { private long id; private String firstName; private String lastName; private Date birthday; public Builder(long id, String lastName) { this.id = id; this.lastName = lastName; } public Builder firstName(String firstName) { this.firstName = firstName; return this; } public Builder birthday(Date birthday) { this.birthday = birthday; return this; } @Override protected Customer buildInternal() { return new Customer(this); } } ... |
The complete source code for this post can be found in my Git repository over at github.com.
6 comments:
Thanks, very useful! :-)
Thanks for your feedback, Thomas :-)
What happend if you do not have birthday when you created customer but wanted to add it later on? What is the setter to call?
The idea here is to have immutable objects, meaning all attribute values must be known at construction time (actually I should have declared the members as final).
If that doesn't work you would indeed need mutator methods.
Great post! I do have a question though, why do you put the annotation under accessors instead of invariants? My understanding is that when instantiating the object, these validators won't intercept on the accessors.
These tips will be useful not just one person.
Emergency Dental Care Clapham
Post a Comment