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)); } }