The Builder Pattern in Java, and Dart Cascades

jvarness

Jake Varness

Posted on December 6, 2017

The Builder Pattern in Java, and Dart Cascades

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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;    
    }
}
Enter fullscreen mode Exit fullscreen mode

This makes your Pizza creation much easier, it ends up looking cleaner, it can help make your Pizzas 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();
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
jvarness
Jake Varness

Posted on December 6, 2017

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related