Jake Varness
Posted on December 6, 2017
Object construction is something that everyone will have to do in a language that has object-oriented paradigms. However, when your objects have a lot of members and are subject to change, how do you create a robust API that protects your consumers from non-passive changes? How do you avoid having multiple constructors that allow your users to construct the objects differently (what I refer to as constructor hell)?
An Example
Let's say that you're opening up your own pizza chain, and you want to write a Java application that allows users to create their own pizza.
The most logical thing for you to do is to create a Pizza
class that allows you to encapsulate the concept of a pizza:
public class Pizza {
private Collection<String> toppings;
private String sauce;
private boolean hasExtraCheese;
}
This looks ok right? And we can create getter and setter methods for these members that allow us to alter the state of the object:
//... other code
public Collection<String> getToppings {
return this.toppings;
}
public void setToppings(Collection<String> toppings) {
this.toppings = toppings;
}
// ... other setters and getters
Not too bad... But how does one construct one of these things?? Well, the simplest answer would be to set everything by hand:
final Pizza pizza = new Pizza();
pizza.hasExtraCheese(true);
pizza.setSauce("garlic");
List<String> toppings = new ArrayList<String>();
toppings.add("pepperoni");
pizza.setToppings(toppings);
Which isn't too bad... But that's a lot of code... We could create a constructor:
public Pizza(Collection<String> toppings, String sauce, boolean hasExtraCheese) {
// and then you set stuff...
}
Which makes the code look more like this:
List<String> toppings = new ArrayList<String>();
toppings.add("pepperoni");
final Pizza pizza = new Pizza(toppings, "marinara", false);
Which isn't too bad... But what if I don't care about specifying if I need extra cheese? And maybe it would be convenient to provide a means of constructing a pizza with a default sauce. At this point, you might be tempted to do the following:
public Pizza(Collection<String> toppings) {
// default the sauce and extra cheese
}
public Pizza(String sauce, boolean hasExtraCheese) {
// default toppings as empty
}
//... potentially many more constructors
You could make so many different constructors as a convenience (anybody who writes Swift get that one?).
Wanna know what makes this constructor hell not so great? When people start wanting to customize their pizza crust.
public class Pizza {
private Collection<String> toppings;
private String sauce;
private boolean hasExtraCheese;
private String crust; // OH NO, NEW THING I DIDN'T PLAN FOR!!! WE'RE DOOMED!
}
Who wants to go write a dozen constructors to support initializing a Pizza
with an optional crust? Who wants to go create exponentially more after marketing tells you people want to customize their pizza with sauce drizzles and crust dust as a means to compete with Pizza Hut?
...
Nobody? Cool, let's write a Builder instead.
Builder Pattern
The Builder pattern allows you to build objects rather than construct them. You provide an API in your builder that allows you to set all of the properties of a Pizza
, and then the builder will build the object for you:
public class PizzaBuilder() {
private Collection<String> toppings;
private String sauce;
private boolean hasExtraCheese;
private String crust;
public PizzaBuilder withToppings(Collection<String> toppings) {
this.toppings = toppings;
return this;
}
// ... create a "with" method for each member you want to set
public Pizza build() {
final Pizza pizza = new Pizza();
// set the pizza properties
return pizza;
}
}
This makes your Pizza
creation much easier, it ends up looking cleaner, it can help make your Pizza
s immutable, and your code is now much more passive to changes:
final Pizza pizza = new PizzaBuilder()
.withHasExtraCheese(true)
.withSauce("marinara")
.withCrust("pan")
.withToppings(new ArrayList<String>())
.build();
Now, when people consume your Pizza
-making API, if you add more functionality, then you won't need to create more constructors, and others won't need to be concerned about implementing the new functionality if they don't have to.
How Dart Addresses This
Dart has some excellent syntax that allows us to skip the creation of builders and prevents us from getting into constructor hell. Let's look at the same Pizza
class in Dart:
class Pizza {
List<String> toppings;
String sauce;
bool hasExtraCheese;
}
One cool thing about Dart is that instance variables implement implicit getters and setters. If the instances are final, setters don't get generated.
And we're done! Our consumers can create Pizza
instances and are already guarded against non-passive changes!
...
No, I'm dead serious. Your job is done. You did the needful. You can go home.
Dart has an excellent feature called cascade notation that allows you to invoke getters, setters, and methods on object instances to instantiate them:
// don't mind me, just constructing a pizza...
var pizza = new Pizza()
..toppings = ['pepperoni', 'mushrooms']
..sauce = 'spaghetti'
..hasExtraCheese = true;
Looks a lot like a builder, but it really isn't. Now, if we add more instance variables, the above code still works correctly. Our consumers can add crust later if they want, no reassembly required.
I hope you enjoyed looking at the builder pattern, and I hope that this has sparked your interest in Dart!
Posted on December 6, 2017
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.