Hi all,
I've been fiddling around with the builder pattern and have combined it with hibernate validator to create immutable value objects. See my example for a typical Address class, it should be pretty self explanatory.
I believe this is a safe and powerful way to build your domain: the mutable inner builder class is used whenever you create objects incrementally from an untrusted source, for example user input from a web form, reading from the db, recieving objects from a web service etc. The immutable outer class is used for all kind of object sharing inside and outside the domain. Additionally it is
always in a valid state.
Comments appreciated.
Code:
package org.schwin;
import org.hibernate.validator.ClassValidator;
import org.hibernate.validator.Length;
import org.hibernate.validator.Min;
import org.hibernate.validator.NotNull;
import org.hibernate.validator.Pattern;
public final class Address {
private final String streetname;
private final int streetnumber;
private final String postalcode;
private final String postalplace;
private Address(final String streetname, final int streetnumber,
final String postalcode, final String postalplace) {
super();
this.streetname = streetname;
this.streetnumber = streetnumber;
this.postalcode = postalcode;
this.postalplace = postalplace;
}
public final Builder builder() {
return new Builder(this);
}
public final String getStreetname() {
return streetname;
}
public final int getStreetnumber() {
return streetnumber;
}
public final String getPostalcode() {
return postalcode;
}
public final String getPostalplace() {
return postalplace;
}
public static final class Builder implements org.schwin.Builder<Address>,
Validator {
private String streetname;
private int streetnumber;
private String postalcode;
private String postalplace;
private boolean valid = false;
private boolean validated = false;
private static final ClassValidator<Builder> validator = new ClassValidator<Builder>(
Builder.class);
public Builder() {
super();
}
private Builder(final Address address) {
super();
this.streetname = address.streetname;
this.streetnumber = address.streetnumber;
this.postalcode = address.postalcode;
this.postalplace = address.postalplace;
}
public final void setStreetname(final String streetname) {
invalidate();
this.streetname = streetname;
}
@Length(min = 1)
public final String getStreetname() {
return streetname;
}
public final void setStreetnumber(final int streetnumber) {
invalidate();
this.streetnumber = streetnumber;
}
@Min(1)
public final int getStreetnumber() {
return streetnumber;
}
@NotNull
@Length(min = 4, max = 8)
@Pattern(regex = "^\\d*$")
public final String getPostalcode() {
return postalcode;
}
public void setPostalcode(final String postalcode) {
invalidate();
this.postalcode = postalcode;
}
@NotNull
@Length(min = 1, max = 50)
public final String getPostalplace() {
return postalplace;
}
public void setPostalplace(final String postalplace) {
invalidate();
this.postalplace = postalplace;
}
private void invalidate() {
validated = false;
}
private void validate() {
valid = 0 == validator.getInvalidValues(this).length;
validated = true;
}
@Override
public final boolean isValid() {
if (!validated)
validate();
return valid;
}
@Override
public final Address build() {
if (!isValid())
throw DEFAULT_INVALID_EXCEPTION;
return new Address(streetname, streetnumber, postalcode,
postalplace);
}
}
}
Code:
package org.schwin;
public interface Builder<T> {
public final static IllegalStateException DEFAULT_INVALID_EXCEPTION = new IllegalStateException(
"build() called while builder was in an invalid state");
public abstract T build() throws IllegalStateException;
}
Code:
package org.schwin;
public interface Validator {
public abstract boolean isValid();
}