I have created what, for me, is a big improvement over Josh Bloch's Builder Pattern. Not to say in any way that it is "better", just that in a very specific situation, it does provide some advantages--the biggest being that it decouples the builder from its to-be-built class.
I have thoroughly documented this alternative below, which I call the Blind Builder Pattern.
Design Pattern: Blind Builder
As an alternative to Joshua Bloch's Builder Pattern (item 2 in Effective Java, 2nd edition), I have created what I call the "Blind Builder Pattern", which shares many of the benefits of the Bloch Builder and, aside from a single character, is used in exactly the same way. Blind Builders have the advantage of
- decoupling the builder from its enclosing class, eliminating a circular dependency,
- greatly reduces the size of the source code of (what is no longer) the enclosing class, and
- allows the
ToBeBuilt
class to be extended without having to extend its builder.
In this documentation, I'll refer to the class-being-built as the "ToBeBuilt
" class.
A class implemented with a Bloch Builder
A Bloch Builder is a public static class
contained inside the class that it builds. An example:
public class UserConfig {
private final String sName ;
private final int iAge ;
private final String sFavColor;
public UserConfig(UserConfig.Cfg uc_c) { //CONSTRUCTOR
//transfer
try {
sName = uc_c.sName;
} catch(NullPointerException rx) {
throw new NullPointerException("uc_c");
}
iAge = uc_c.iAge;
sFavColor = uc_c.sFavColor;
//VALIDATE ALL FIELDS HERE
}
public String toString() {
return "name=" + sName + ", age=" + iAge + ", sFavColor=" + sFavColor;
}
//builder...START
public static class Cfg {
private String sName ;
private int iAge ;
private String sFavColor;
public Cfg(String s_name) {
sName = s_name;
}
//self-returning setters...START
public Cfg age(int i_age) {
iAge = i_age;
return this;
}
public Cfg favoriteColor(String s_color) {
sFavColor = s_color;
return this;
}
//self-returning setters...END
public UserConfig build() {
return (new UserConfig(this));
}
}
//builder...END
}
Instantiating a class with a Bloch Builder
UserConfig uc = new UserConfig.Cfg("Kermit").age(50).favoriteColor("green").build();
The same class, implemented as a Blind Builder
There are three parts to a Blind Builder, each of which is in a separate source-code file:
- The
ToBeBuilt
class (in this example: UserConfig
)
- Its "
Fieldable
" interface
- The builder
1. The to-be-built class
The to-be-built class accepts its Fieldable
interface as its only constructor parameter. The constructor sets all internal fields from it, and validates each. Most importantly, this ToBeBuilt
class has no knowledge of its builder.
public class UserConfig {
private final String sName ;
private final int iAge ;
private final String sFavColor;
public UserConfig(UserConfig_Fieldable uc_f) { //CONSTRUCTOR
//transfer
try {
sName = uc_f.getName();
} catch(NullPointerException rx) {
throw new NullPointerException("uc_f");
}
iAge = uc_f.getAge();
sFavColor = uc_f.getFavoriteColor();
//VALIDATE ALL FIELDS HERE
}
public String toString() {
return "name=" + sName + ", age=" + iAge + ", sFavColor=" + sFavColor;
}
}
As noted by one smart commenter (who inexplicably deleted their answer), if the ToBeBuilt
class also implements its Fieldable
, its one-and-only constructor can be used as both its primary and copy constructor (a disadvantage is that fields are always validated, even though it is known that the fields in the original ToBeBuilt
are valid).
2. The "Fieldable
" interface
The fieldable interface is the "bridge" between the ToBeBuilt
class and its builder, defining all fields necessary to build the object. This interface is required by the ToBeBuilt
classes constructor, and is implemented by the builder. Since this interface may be implemented by classes other than the builder, any class may easily instantiate the ToBeBuilt
class, without being forced to use its builder. This also makes it easier to extend the ToBeBuilt
class, when extending its builder is not desirable or necessary.
As described in a below section, I do not document the functions in this interface at all.
public interface UserConfig_Fieldable {
String getName();
int getAge();
String getFavoriteColor();
}
3. The builder
The builder implements the Fieldable
class. It does no validation at all, and to emphasize this fact, all of its fields are public and mutable. While this public accessibility is not a requirement, I prefer and recommend it, because it re-enforces the fact that validation does not occur until the ToBeBuilt
's constructor is called. This is important, because it is possible for another thread to manipulate the builder further, before it is passed to the ToBeBuilt
's constructor. The only way to guarantee the fields are valid--assuming the builder cannot somehow "lock" its state--is for the ToBeBuilt
class to do the final check.
Finally, as with the Fieldable
interface, I do not document any of its getters.
public class UserConfig_Cfg implements UserConfig_Fieldable {
public String sName ;
public int iAge ;
public String sFavColor;
public UserConfig_Cfg(String s_name) {
sName = s_name;
}
//self-returning setters...START
public UserConfig_Cfg age(int i_age) {
iAge = i_age;
return this;
}
public UserConfig_Cfg favoriteColor(String s_color) {
sFavColor = s_color;
return this;
}
//self-returning setters...END
//getters...START
public String getName() {
return sName;
}
public int getAge() {
return iAge;
}
public String getFavoriteColor() {
return sFavColor;
}
//getters...END
public UserConfig build() {
return (new UserConfig(this));
}
}
Instantiating a class with a Blind Builder
UserConfig uc = new UserConfig_Cfg("Kermit").age(50).favoriteColor("green").build();
The only difference is "UserConfig_Cfg
" instead of "UserConfig.Cfg
"
Notes
Disadvantages:
- Blind Builders cannot access private members of its
ToBeBuilt
class,
- They are more verbose, as getters are now required in both the builder and in the interface.
- Everything for a single class is no longer in just one place.
Compiling a Blind Builder is straight-forward:
ToBeBuilt_Fieldable
ToBeBuilt
ToBeBuilt_Cfg
The Fieldable
interface is entirely optional
For a ToBeBuilt
class with few required fields--such as this UserConfig
example class, the constructor could simply be
public UserConfig(String s_name, int i_age, String s_favColor) {
And called in the builder with
public UserConfig build() {
return (new UserConfig(getName(), getAge(), getFavoriteColor()));
}
Or even by eliminating the getters (in the builder) altogether:
return (new UserConfig(sName, iAge, sFavoriteColor));
By passing fields directly, the ToBeBuilt
class is just as "blind" (unaware of its builder) as it is with the Fieldable
interface. However, for ToBeBuilt
classes which and are intended to be "extended and sub-extended many times" (which is in the title of this post), any changes to any field necessitates changes in every sub-class, in every builder and ToBeBuilt
constructor. As the number of fields and sub-classes increases, this becomes impractical to maintain.
(Indeed, with few necessary fields, using a builder at all may be overkill. For those interested, here is a sampling of some of the larger Fieldable interfaces in my personal library.)
Secondary classes in sub-package
I choose to have all builder and the Fieldable
classes, for all Blind Builders, in a sub-package of their ToBeBuilt
class. The sub-package is always named "z
". This prevents these secondary classes from cluttering up the JavaDoc package list. For example
library.class.my.UserConfig
library.class.my.z.UserConfig_Fieldable
library.class.my.z.UserConfig_Cfg
Validation example
As mentioned above, all validation occurs in the ToBeBuilt
's constructor. Here is the constructor again with example validation code:
public UserConfig(UserConfig_Fieldable uc_f) {
//transfer
try {
sName = uc_f.getName();
} catch(NullPointerException rx) {
throw new NullPointerException("uc_f");
}
iAge = uc_f.getAge();
sFavColor = uc_f.getFavoriteColor();
//validate (should really pre-compile the patterns...)
try {
if(!Pattern.compile("\\w+").matcher(sName).matches()) {
throw new IllegalArgumentException("uc_f.getName() (\"" + sName + "\") may not be empty, and must contain only letters digits and underscores.");
}
} catch(NullPointerException rx) {
throw new NullPointerException("uc_f.getName()");
}
if(iAge < 0) {
throw new IllegalArgumentException("uc_f.getAge() (" + iAge + ") is less than zero.");
}
try {
if(!Pattern.compile("(?:red|blue|green|hot pink)").matcher(sFavColor).matches()) {
throw new IllegalArgumentException("uc_f.getFavoriteColor() (\"" + uc_f.getFavoriteColor() + "\") is not red, blue, green, or hot pink.");
}
} catch(NullPointerException rx) {
throw new NullPointerException("uc_f.getFavoriteColor()");
}
}
Documenting Builders
This section is applicable to both Bloch Builders and Blind Builders. It demonstrates how I document the classes in this design, making setters (in the builder) and their getters (in the ToBeBuilt
class) directly cross-referenced to each other--with a single mouse-click, and without the user needing to know where those functions actually reside--and without the developer having to document anything redundantly.
Getters: In the ToBeBuilt
classes only
Getters are documented only in the ToBeBuilt
class. The equivalent getters both in the _Fieldable
and _Cfg
classes are ignored. I don't document them at all.
/**
<P>The user's age.</P>
@return An int representing the user's age.
@see UserConfig_Cfg#age(int)
@see getName()
**/
public int getAge() {
return iAge;
}
The first @see
is a link to its setter, which is in the builder class.
Setters: In the builder-class
The setter is documented as if it is in the ToBeBuilt
class, and also as if it does the validation (which really is done by the ToBeBuilt
's constructor). The asterisk ("*
") is a visual clue indicating that the link's target is in another class.
/**
<P>Set the user's age.</P>
@param i_age May not be less than zero. Get with {@code UserConfig#getName() getName()}*.
@see #favoriteColor(String)
**/
public UserConfig_Cfg age(int i_age) {
iAge = i_age;
return this;
}
Further information
Putting it all together: The full source of the Blind Builder example, with complete documentation
UserConfig.java
import java.util.regex.Pattern;
/**
<P>Information about a user -- <I>[builder: UserConfig_Cfg]</I></P>
<P>Validation of all fields occurs in this classes constructor. However, each validation requirement is document only in the builder's setter functions.</P>
<P>{@code java xbn.z.xmpl.lang.builder.finalv.UserConfig}</P>
**/
public class UserConfig {
public static final void main(String[] igno_red) {
UserConfig uc = new UserConfig_Cfg("Kermit").age(50).favoriteColor("green").build();
System.out.println(uc);
}
private final String sName ;
private final int iAge ;
private final String sFavColor;
/**
<P>Create a new instance. This sets and validates all fields.</P>
@param uc_f May not be {@code null}.
**/
public UserConfig(UserConfig_Fieldable uc_f) {
//transfer
try {
sName = uc_f.getName();
} catch(NullPointerException rx) {
throw new NullPointerException("uc_f");
}
iAge = uc_f.getAge();
sFavColor = uc_f.getFavoriteColor();
//validate
try {
if(!Pattern.compile("\\w+").matcher(sName).matches()) {
throw new IllegalArgumentException("uc_f.getName() (\"" + sName + "\") may not be empty, and must contain only letters digits and underscores.");
}
} catch(NullPointerException rx) {
throw new NullPointerException("uc_f.getName()");
}
if(iAge < 0) {
throw new IllegalArgumentException("uc_f.getAge() (" + iAge + ") is less than zero.");
}
try {
if(!Pattern.compile("(?:red|blue|green|hot pink)").matcher(sFavColor).matches()) {
throw new IllegalArgumentException("uc_f.getFavoriteColor() (\"" + uc_f.getFavoriteColor() + "\") is not red, blue, green, or hot pink.");
}
} catch(NullPointerException rx) {
throw new NullPointerException("uc_f.getFavoriteColor()");
}
}
//getters...START
/**
<P>The user's name.</P>
@return A non-{@code null}, non-empty string.
@see UserConfig_Cfg#UserConfig_Cfg(String)
@see #getAge()
@see #getFavoriteColor()
**/
public String getName() {
return sName;
}
/**
<P>The user's age.</P>
@return A number greater-than-or-equal-to zero.
@see UserConfig_Cfg#age(int)
@see #getName()
**/
public int getAge() {
return iAge;
}
/**
<P>The user's favorite color.</P>
@return A non-{@code null}, non-empty string.
@see UserConfig_Cfg#age(int)
@see #getName()
**/
public String getFavoriteColor() {
return sFavColor;
}
//getters...END
public String toString() {
return "getName()=" + getName() + ", getAge()=" + getAge() + ", getFavoriteColor()=" + getFavoriteColor();
}
}
UserConfig_Fieldable.java
/**
<P>Required by the {@link UserConfig} {@code UserConfig#UserConfig(UserConfig_Fieldable) constructor}.</P>
**/
public interface UserConfig_Fieldable {
String getName();
int getAge();
String getFavoriteColor();
}
UserConfig_Cfg.java
import java.util.regex.Pattern;
/**
<P>Builder for {@link UserConfig}.</P>
<P>Validation of all fields occurs in the <CODE>UserConfig</CODE> constructor. However, each validation requirement is document only in this classes setter functions.</P>
**/
public class UserConfig_Cfg implements UserConfig_Fieldable {
public String sName ;
public int iAge ;
public String sFavColor;
/**
<P>Create a new instance with the user's name.</P>
@param s_name May not be {@code null} or empty, and must contain only letters, digits, and underscores. Get with {@code UserConfig#getName() getName()}{@code ()}.
**/
public UserConfig_Cfg(String s_name) {
sName = s_name;
}
//self-returning setters...START
/**
<P>Set the user's age.</P>
@param i_age May not be less than zero. Get with {@code UserConfig#getName() getName()}{@code ()}.
@see #favoriteColor(String)
**/
public UserConfig_Cfg age(int i_age) {
iAge = i_age;
return this;
}
/**
<P>Set the user's favorite color.</P>
@param s_color Must be {@code "red"}, {@code "blue"}, {@code green}, or {@code "hot pink"}. Get with {@code UserConfig#getName() getName()}{@code ()}*.
@see #age(int)
**/
public UserConfig_Cfg favoriteColor(String s_color) {
sFavColor = s_color;
return this;
}
//self-returning setters...END
//getters...START
public String getName() {
return sName;
}
public int getAge() {
return iAge;
}
public String getFavoriteColor() {
return sFavColor;
}
//getters...END
/**
<P>Build the UserConfig, as configured.</P>
@return <CODE>(new {@link UserConfig#UserConfig(UserConfig_Fieldable) UserConfig}(this))</CODE>
**/
public UserConfig build() {
return (new UserConfig(this));
}
}