A while back I posted a basic Builder example on Stack Overflow. User Marcel Stör complained:
Problem with this approach is that you need to define all the members twice, once in the builder and once in the actual class. [This answer] shows how this can be avoided. With that other approach, however, you cannot have final members although the object itself is still immutable.
Well, I looked at that answer, from user Yishai, and I didn’t like it.
I don’t know, I’m not a fan of that approach — the builder’s not reusable, it’s not thread-safe, and you can’t design the build product class to guarantee it’s fully initialized at construction. If I was really worried about DRY, I’d probably use an immutable (copy-on-write) builder and make the last builder instance double as the build product’s immutable data.
After I wrote that, I thought I should put some code where my mouth was. So: here’s the original version (more or less), with the fields declared in both the Builder
and its product, the Widget
:
public class Widget implements IWidget { public static class Builder implements IWidgetBuilder<Widget> { private String name; private String model; private String upc; private double price; private Manufacturer manufacturer; public Builder ( String name, double price ) { this.name = name; this.price = price; } @Override public Widget build () { Widget product = new Widget( this ); validate( product ); return product; } @Override public Builder manufacturer ( Manufacturer value ) { this.manufacturer = value; return this; } @Override public Builder upc ( String value ) { this.upc = value; return this; } @Override public Builder model ( String value ) { this.model = value; return this; } private static void validate ( Widget product ) { assert product.getName() != null : "Product must have a name"; assert product.getPrice() >= 0 : "Product price cannot be negative"; Manufacturer mfgr = product.getManufacturer(); if ( mfgr != null ) { String upc = product.getUpc(); assert upc != null; assert upc.startsWith( mfgr.getCode() ) : "Product UPC does not match manufacturer"; } } } private final String name; private final String model; private final String upc; private final double price; private final Manufacturer manufacturer; /** * Creates an immutable widget instance. */ private Widget ( Builder b ) { this.name = b.name; this.price = b.price; this.model = b.model; this.upc = b.upc; this.manufacturer = b.manufacturer; } public Manufacturer getManufacturer () { return manufacturer; } public String getModel () { return model; } public String getName () { return name; } public double getPrice () { return price; } public String getUpc () { return upc; } }
And here’s the new version: the fields are only declared in the Builder
, and they’re always final
. The Builder
itself is immutable, using a copy-on-write pattern, and the last Builder
instance becomes the state of the Widget
it produces.
public class Widget2 implements IWidget { public static class Builder implements IWidgetBuilder<Widget2> { private final String name; private final String model; private final String upc; private final double price; private final Manufacturer manufacturer; public Builder ( String name, double price ) { this( name, null, null, price, null ); } private Builder ( String name, String model, String upc, double price, Manufacturer manufacturer ) { this.manufacturer = manufacturer; this.model = model; this.name = name; this.price = price; this.upc = upc; } public Widget2 build () { Widget2 product = new Widget2( this ); validate( product ); return product; } public Builder manufacturer ( Manufacturer value ) { return new Builder( this.name, this.model, this.upc, this.price, value ); } public Builder upc ( String value ) { return new Builder( this.name, this.model, value, this.price, this.manufacturer ); } public Builder model ( String value ) { return new Builder( this.name, value, this.upc, this.price, this.manufacturer ); } private static void validate ( Widget2 product ) { assert product.getName() != null : "Product must have a name"; assert product.getPrice() >= 0 : "Product price cannot be negative"; Manufacturer mfgr = product.getManufacturer(); if ( mfgr != null ) { String upc = product.getUpc(); assert upc != null; assert upc.startsWith( mfgr.getCode() ) : "Product UPC does not match manufacturer"; } } } private final Builder b; /** * Creates an immutable widget instance. */ private Widget2 ( Builder b ) { this.b = b; } @Override public Manufacturer getManufacturer () { return b.manufacturer; } @Override public String getModel () { return b.model; } @Override public String getName () { return b.name; } @Override public double getPrice () { return b.price; } @Override public String getUpc () { return b.upc; } }
Voilà, DRY. Sadly, it only ends up one line shorter (thanks to the extra Builder
constructor), but at least it doesn’t define anything twice!