An overview of the Builder design pattern and its implementation in Dart and Flutter
Previously in the series, I analysed a relatively complex, but very practical and useful structural design pattern — Bridge. This time I would like to represent a design pattern, which divides the construction of a complex object into several separate steps. It is a creational design pattern called Builder.
To see all the design patterns in action, check the Flutter Design Patterns application.
What is the Builder design pattern?​
Builder is a creational design pattern, which intention in the GoF book is described like this:
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
The intention could be split into two parts:
Separate the construction of a complex object from its representation…
You should have already noticed this in the majority of the design patterns overviewed in the series, maybe in a slightly different form, e.g. separate the abstraction from its representation. In this case, one of the main purposes of the Builder design pattern is to separate the creation process (logic) of a complex object from the object (data) itself. What is a complex object? Well, there is no specific point or any criteria of the object when you could say that it is complex. Usually, an object could be considered complex when the object's creation does not simply end with calling its constructor - additionally, you should set some extra specific parameters, and call additional methods.
Ok, at this point we have a complex object, we can create it by using some additional parameters and/or methods - why do we need any additional abstraction on top of that, and why we should separate this creation process from the object at all?
… so that the same construction process can create different representations.
Ahh, that's the point! To understand it better, let's say we are building a house. To build a house (object), the construction steps (build logic) are pretty much the same - you need the foundation, floor, some walls, doors, windows, a roof, etc. Even though the construction process of the house is the same, each of these steps could be adjusted, hence the final result would look completely different. And that is the main idea of the Builder design pattern - abstract the object's creation process so that construction steps could be adjusted to provide a different representation (final result).
The Builder design pattern moves the object construction code out of its own class to separate objects called builders. Each of these builders follows the same interface and implements separate object construction steps. That is, if you want a different object's representation, just create a different builder class and implement these construction steps correspondingly. Also, there is one additional layer in the Builder design pattern - Director. The Director is a simple class that is aware of the Builder interface and defines the order in which to execute the building steps. This class is not mandatory, though, but it hides the details of product construction from the client code.
I know, the structure of the Builder design pattern is quite complex, so let's move to the analysis and implementation parts to understand it better!
Analysis​
The general structure of the Builder design pattern looks like this:
- Builder - defines an abstract interface that is common to all types of builders for creating parts of a Product;
- Concrete Builder - provides a specific implementation of the construction steps. Also, it defines and keeps track of the Product it creates;
- Director - constructs an object using the Builder interface, defines the order in which the construction steps are called;
- Product - represents the complex object under construction, exposes interface/methods for assembling the parts into the final result;
- Client - associates the specific Builder object with the Director. Later, a Product object is created by calling the Director class instance.
Applicability​
The Builder design pattern should be used when you notice multiple constructors of the same class referencing each other. For instance, you have a constructor with multiple optional parameters. Some of these parameters have default values, so you create several shorter constructors with fewer parameters, but still refer to the main one. By using the Builder design pattern, you are building objects step by step only using those steps that are really needed - you do not need to cope with the problem of multiple constructors with optional parameters anymore.
As already mentioned, this pattern should be used when you want to create different representations of some product. That is, the pattern could be applied when the construction steps are similar but differ in details. The builder interface defines those steps (some of them may even have the default implementation) while concrete builders implement these steps to construct a particular representation of the product.
Finally, the Builder design pattern is useful when the algorithm for creating a complex object should be independent of the parts that make up the objects and how they are assembled. In simple words, it is just a simple extraction of the object's creation logic from its own class. Therefore, the construction algorithm could evolve separately from the actual product it provides, the modification of this process does require changing the object's code.
Implementation​
This time, the implementation part is very straightforward - we will use the Builder design pattern to implement the build process of McDonald's burgers.
As you may know, McDonald's menu contains multiple burgers (a regular burger, cheeseburger, and Big Mac just to name a few). All of these burgers use the very same products, just the ingredients list is different:
- Regular burger - buns, beef patty, ketchup, mustard, grill seasoning, onions, pickle slices;
- Cheeseburger - buns, beef patty, cheese, ketchup, mustard, grill seasoning, onions, pickle slices;
- Big Mac - buns, cheese, beef patty, Big Mac sauce, grill seasoning, onions, pickle slices, shredded lettuce.
As you can see, by changing the build process of the burger (changing ingredients), we completely change the final result. Also, at any moment there could be a requirement to add a new burger to the menu. Finally, the user-friendly UI should be implemented where you can select a burger from the menu and see its price, ingredients and allergens list.
For this problem, the Builder design pattern is a great option since we can define different builder classes which build specific burgers. Also, if a new burger should be added to the menu at any point, we can simply introduce another builder class to cope with this change. Let's check the class diagram first and then implement the pattern.
Class diagram​
The class diagram below shows the implementation of the Builder design pattern:
The Ingredient
is an abstract class that is used as a base class for all the ingredient classes. The class contains allergens
and name
properties as well as getAllergens()
and getName()
methods to return the values of these properties.
There are a lot of concrete ingredient classes: BigMacBun
, RegularBun
, BeefPatty
, McChickenPatty
, BigMacSauce
, Ketchup
, Mayonnaise
, Mustard
, Onions
, PickleSlices
, ShreddedLettuce
, Cheese
and GrillSeasoning
. All of these classes represent a specific ingredient of a burger and contain a default constructor to set the allergens
and name
property values of the base class.
The Burger
is a simple class representing the product of a builder. It contains the ingredients
list and price
property to store the corresponding values. Also, the class contains several methods:
addIngredient()
 - adds an ingredient to the burger;getFormattedIngredients()
 - returns a formatted ingredients' list of a burger (separated by commas);getFormattedAllergens()
 - returns a formatted allergens' list of a burger (separated by commas);getFormattedPrice()
 - returns a formatted price of a burger;setPrice()
 - sets the price for the burger.
BurgerBuilderBase
is an abstract class that is used as a base class for all the burger builder classes. It contains burger
and price
properties to store the final product - burger - and its price correspondingly. Additionally, the class stores some methods with default implementation:
createBurger()
 - initialises aBurger
class object;getBurger()
 - returns the built burger result;setBurgerPrice()
 - sets the price for the burger object.
BurgerBuilderBase
also contain several abstract methods which must be implemented in the specific implementation classes of the burger builder: addBuns()
, addCheese()
, addPatties()
, addSauces()
, addSeasoning()
and addVegetables()
.
BigMacBuilder
, CheeseburgerBuilder
, HamburgerBuilder
and McChickenBuilder
are concrete builder classes that extend the abstract class BurgerBuilderBase
and implement its abstract methods.
BurgerMaker
is a director class that manages the burger's build process. It contains a specific builder implementation as a burgerBuilder
property, prepareBurger()
method to build the burger and a getBurger()
method to return it. Also, the builder's implementation could be changed using the changeBurgerBuilder()
method.
BuilderExample
initialises and contains the BurgerMaker
class. Also, it references all the specific burger builders which could be changed at run-time using the UI dropdown selection.
Ingredient​
An abstract class that stores the allergens
, and name
fields and is extended by all of the ingredient classes.
abstract class Ingredient {
late List<String> allergens;
late String name;
List<String> getAllergens() {
return allergens;
}
String getName() {
return name;
}
}
Concrete ingredients​
All of these classes represent a specific ingredient by extending the Ingredient
class and specifying an allergens list as well as the name value.
- Big Mac bun:
class BigMacBun extends Ingredient {
BigMacBun() {
name = 'Big Mac Bun';
allergens = ['Wheat'];
}
}
- Regular bun:
class RegularBun extends Ingredient {
RegularBun() {
name = 'Regular Bun';
allergens = ['Wheat'];
}
}
- Cheese:
class Cheese extends Ingredient {
Cheese() {
name = 'Cheese';
allergens = ['Milk', 'Soy'];
}
}
- Grill seasoning:
class GrillSeasoning extends Ingredient {
GrillSeasoning() {
name = 'Grill Seasoning';
allergens = [];
}
}
- Beef patty:
class BeefPatty extends Ingredient {
BeefPatty() {
name = 'Beef Patty';
allergens = [];
}
}
- McChicken patty:
class McChickenPatty extends Ingredient {
McChickenPatty() {
name = 'McChicken Patty';
allergens = [
'Wheat',
'Cooked in the same fryer that we use for Buttermilk Crispy Chicken which contains a milk allergen'
];
}
}
- Big Mac sauce:
class BigMacSauce extends Ingredient {
BigMacSauce() {
name = 'Big Mac Sauce';
allergens = ['Egg', 'Soy', 'Wheat'];
}
}
- Ketchup:
class Ketchup extends Ingredient {
Ketchup() {
name = 'Ketchup';
allergens = [];
}
}
- Mayonnaise:
class Mayonnaise extends Ingredient {
Mayonnaise() {
name = 'Mayonnaise';
allergens = ['Egg'];
}
}
- Mustard:
class Mustard extends Ingredient {
Mustard() {
name = 'Mustard';
allergens = [];
}
}
- Onions:
class Onions extends Ingredient {
Onions() {
name = 'Onions';
allergens = [];
}
}
- Pickle slices:
class PickleSlices extends Ingredient {
PickleSlices() {
name = 'Pickle Slices';
allergens = [];
}
}
- Shredded lettuce:
class ShreddedLettuce extends Ingredient {
ShreddedLettuce() {
name = 'Shredded Lettuce';
allergens = [];
}
}
Burger​
A simple class to store information about the burger: its price and a list of ingredients it contains. Also, class methods, such as getFormattedIngredients()
, getFormattedAllergens()
and getFormattedPrice()
, returns these values in human-readable format.
class Burger {
final List<Ingredient> _ingredients = [];
late double _price;
void addIngredient(Ingredient ingredient) {
_ingredients.add(ingredient);
}
String getFormattedIngredients() {
return _ingredients.map((x) => x.getName()).join(', ');
}
String getFormattedAllergens() {
final allergens = <String>{};
for (final ingredient in _ingredients) {
allergens.addAll(ingredient.getAllergens());
}
return allergens.join(', ');
}
String getFormattedPrice() {
return '\$${_price.toStringAsFixed(2)}';
}
// ignore: use_setters_to_change_properties
void setPrice(double price) {
_price = price;
}
}
BurgerBuilderBase​
An abstract class that stores burger
and price
properties defines some default methods to create/return the burger object and set its price. Also, the class defines several abstract methods which must be implemented by the derived burger builder classes.
abstract class BurgerBuilderBase {
late Burger burger;
late double price;
void createBurger() {
burger = Burger();
}
Burger getBurger() {
return burger;
}
void setBurgerPrice() {
burger.setPrice(price);
}
void addBuns();
void addCheese();
void addPatties();
void addSauces();
void addSeasoning();
void addVegetables();
}
Concrete builders​
BigMacBuilder
 - builds a Big Mac using the following ingredients: BigMacBun
, Cheese
, BeefPatty
, BigMacSauce
, GrillSeasoning
, Onions
, PickleSlices
and ShreddedLettuce
.
class BigMacBuilder extends BurgerBuilderBase {
BigMacBuilder() {
price = 3.99;
}
void addBuns() {
burger.addIngredient(BigMacBun());
}
void addCheese() {
burger.addIngredient(Cheese());
}
void addPatties() {
burger.addIngredient(BeefPatty());
}
void addSauces() {
burger.addIngredient(BigMacSauce());
}
void addSeasoning() {
burger.addIngredient(GrillSeasoning());
}
void addVegetables() {
burger.addIngredient(Onions());
burger.addIngredient(PickleSlices());
burger.addIngredient(ShreddedLettuce());
}
}
CheeseburgerBuilder
 - builds a cheeseburger using the following ingredients: RegularBun
, Cheese
, BeefPatty
, Ketchup
, Mustard
, GrillSeasoning
, Onions
and PickleSlices
.
class CheeseburgerBuilder extends BurgerBuilderBase {
CheeseburgerBuilder() {
price = 1.09;
}
void addBuns() {
burger.addIngredient(RegularBun());
}
void addCheese() {
burger.addIngredient(Cheese());
}
void addPatties() {
burger.addIngredient(BeefPatty());
}
void addSauces() {
burger.addIngredient(Ketchup());
burger.addIngredient(Mustard());
}
void addSeasoning() {
burger.addIngredient(GrillSeasoning());
}
void addVegetables() {
burger.addIngredient(Onions());
burger.addIngredient(PickleSlices());
}
}
HamburgerBuilder
 - builds a cheeseburger using the following ingredients: RegularBun
, BeefPatty
, Ketchup
, Mustard
, GrillSeasoning
, Onions
and PickleSlices
. The addCheese()
method is not relevant for this builder, hence the implementation is not provided (skipped).
class HamburgerBuilder extends BurgerBuilderBase {
HamburgerBuilder() {
price = 1.0;
}
void addBuns() {
burger.addIngredient(RegularBun());
}
void addCheese() {
// Not needed
}
void addPatties() {
burger.addIngredient(BeefPatty());
}
void addSauces() {
burger.addIngredient(Ketchup());
burger.addIngredient(Mustard());
}
void addSeasoning() {
burger.addIngredient(GrillSeasoning());
}
void addVegetables() {
burger.addIngredient(Onions());
burger.addIngredient(PickleSlices());
}
}
McChickenBuilder
 - builds a cheeseburger using the following ingredients: RegularBun
, McChickenPatty
, Mayonnaise
and ShreddedLettuce
. The addCheese()
and addSeasoning()
methods are not relevant for this builder, hence the implementation is not provided (skipped).
class McChickenBuilder extends BurgerBuilderBase {
McChickenBuilder() {
price = 1.29;
}
void addBuns() {
burger.addIngredient(RegularBun());
}
void addCheese() {
// Not needed
}
void addPatties() {
burger.addIngredient(McChickenPatty());
}
void addSauces() {
burger.addIngredient(Mayonnaise());
}
void addSeasoning() {
// Not needed
}
void addVegetables() {
burger.addIngredient(ShreddedLettuce());
}
}
BurgerMaker​
A director class that manages the burger's build process and returns the build result. A specific implementation of the builder is injected into the class via the constructor.
class BurgerMaker {
BurgerBuilderBase burgerBuilder;
BurgerMaker(this.burgerBuilder);
// ignore: use_setters_to_change_properties
void changeBurgerBuilder(BurgerBuilderBase burgerBuilder) {
this.burgerBuilder = burgerBuilder;
}
Burger getBurger() {
return burgerBuilder.getBurger();
}
void prepareBurger() {
burgerBuilder.createBurger();
burgerBuilder.setBurgerPrice();
burgerBuilder.addBuns();
burgerBuilder.addCheese();
burgerBuilder.addPatties();
burgerBuilder.addSauces();
burgerBuilder.addSeasoning();
burgerBuilder.addVegetables();
}
}
Example​
First of all, a markdown file is prepared and provided as a pattern's description:
BuilderExample
initialises and contains the BurgerMaker
class object. Also, it contains a list of BurgerMenuItem
objects/selection items which is used to select the specific builder using UI.
class BuilderExample extends StatefulWidget {
const BuilderExample();
_BuilderExampleState createState() => _BuilderExampleState();
}
class _BuilderExampleState extends State<BuilderExample> {
final BurgerMaker _burgerMaker = BurgerMaker(HamburgerBuilder());
final List<BurgerMenuItem> _burgerMenuItems = [];
late BurgerMenuItem _selectedBurgerMenuItem;
late Burger _selectedBurger;
void initState() {
super.initState();
_burgerMenuItems.addAll([
BurgerMenuItem(
label: 'Hamburger',
burgerBuilder: HamburgerBuilder(),
),
BurgerMenuItem(
label: 'Cheeseburger',
burgerBuilder: CheeseburgerBuilder(),
),
BurgerMenuItem(
label: 'Big Mac\u00AE',
burgerBuilder: BigMacBuilder(),
),
BurgerMenuItem(
label: 'McChicken\u00AE',
burgerBuilder: McChickenBuilder(),
)
]);
_selectedBurgerMenuItem = _burgerMenuItems[0];
_selectedBurger = _prepareSelectedBurger();
}
Burger _prepareSelectedBurger() {
_burgerMaker.prepareBurger();
return _burgerMaker.getBurger();
}
void _onBurgerMenuItemChanged(BurgerMenuItem? selectedItem) {
setState(() {
_selectedBurgerMenuItem = selectedItem!;
_burgerMaker.changeBurgerBuilder(selectedItem.burgerBuilder);
_selectedBurger = _prepareSelectedBurger();
});
}
Widget build(BuildContext context) {
return ScrollConfiguration(
behavior: const ScrollBehavior(),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: LayoutConstants.paddingL,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Text(
'Select menu item:',
style: Theme.of(context).textTheme.headline6,
),
],
),
DropdownButton(
value: _selectedBurgerMenuItem,
items: _burgerMenuItems
.map<DropdownMenuItem<BurgerMenuItem>>(
(BurgerMenuItem item) => DropdownMenuItem(
value: item,
child: Text(item.label),
),
)
.toList(),
onChanged: _onBurgerMenuItemChanged,
),
const SizedBox(height: LayoutConstants.spaceL),
Row(
children: <Widget>[
Text(
'Information:',
style: Theme.of(context).textTheme.headline6,
),
],
),
const SizedBox(height: LayoutConstants.spaceM),
BurgerInformationColumn(burger: _selectedBurger),
],
),
),
);
}
}
The director class BurgerMaker
does not care about the specific implementation of the builder - the specific implementation could be changed at run-time, hence providing a different result. Also, this kind of implementation allows easily adding a new builder (as long as it extends the BurgerBuilderBase
class) to provide another different product's representation without breaking the existing code.
As you can see in the example when a specific builder is selected from the dropdown list, a new product (burger) is created and its information is provided in the UI - price, ingredients and allergens.
All of the code changes for the Builder design pattern and its example implementation could be found here.
To see the pattern in action, check the interactive Builder example.