Everyone coming from OOP programming have heard about the builder pattern... "and its intent of separating the construction of a complex objets from its representation, providing a flexible solution for creating objects to programmers".
My first steps with OOP was with Java and trust me it was while ago. Nowadays, I pretty much code in Javascript (React & Typescript) and a week ago I decided to give a try Dart...!
Okay buddy, but...! I know, I know "Builder pattern"!
There is a great Dev article introducing cascade notation in Dart, which cover in somehow an approach to the Builder pattern, but in my desire to discover more about this new language this post explain my own implementation of the Builder pattern in Dart.
My example is based on the 🍕
classfrom the mentioned article, in this way it will help to connect both articles and if you have read it before this one, you will be already familiar with the codeIf you prefer to go directly to the code and see what my brain coded, see this gist
First attempts
Thinking on a common implementation of a Builder pattern in Java my first attempt was to write a class that contained a static nested builder class such as:
class Pizza {
Pizza._();
static class Builder {
Builder();
}
}
Should be easy... but "arg!!" my first error came up: classes can't be declared inside other classes
So, researching about this error my surprise was that Dart doesn't allow to declare nested classes. Okay, well... maybe just use the cascade notation and get this sorted out.
NO WAY... I need to find a work around it!
but why? what are the reasons...?
- Immutability issues
- cascade notation allows to modify/mutate the value of the
class Pizzaattributes ones it has been instantiated/"built" (classes in Dart auto-create setters methods for each of their attributes if they are not declared asfinal). So, using it breaks one of the purpose of the builder pattern: build it once and keep it as immutable across its life cycle. - Those attributes declared as
finalcould not be modified by the cascade notation (there is not setter method), so they will need to be initialised into the constructor... mmm this smells as another reason of why the builder pattern was created, isn't it?
- cascade notation allows to modify/mutate the value of the
- Messy code
- there is not separation between those methods used for building the
Pizzaobject and those which express what a pizza could "do" or what could be done with a Pizza.
- there is not separation between those methods used for building the
Dart Builder pattern
There are two main code blocks:
-
pizza.dart which contains the
class Pizza&class PizzaBuildercode -
test.dart which contains code explaining how to use them and showing the output of the
printstatements from the test.dart code blocks
/// pizza.dart
class Pizza {
final String sauce;
final List<String> toppings;
final bool hasExtraCheese;
Pizza._builder(PizzaBuilder builder) :
sauce = builder.sauce,
toppings = builder.toppings,
hasExtraCheese = builder.hasExtraCheese;
}
class PizzaBuilder {
static const String neededTopping = 'cheese';
final String sauce;
PizzaBuilder(this.sauce);
List<String> toppings;
bool hasExtraCheese;
void setToppings(List<String> toppings) {
if (!toppings.contains(neededTopping)) {
throw 'Really, without $neededTopping? :(';
}
this.toppings = toppings;
}
Pizza build() {
return Pizza._builder(this);
}
}
class Pizza
Ups:
- Clean code and concept separation
-
class Pizzadeclares their attributes asfinal, so they can't be modified ones they have been instantiated or built -
class Pizzaredefines its default constructor, declaring a named constructor_builder, which warns devs that this class must be built through the Builder pattern. In addiction, it is marked as private (library scoped) with the_and it does not allow to create direct instances of itself as there is not default constructor defined - Injects the
PizzaBuilderas attribute of thePizzaconstructor initialing thefinalattributes invoking a superclass constructor using the initializer-list mode - Because attributes are declared as
finalthey don't need to be private, so we could access them directly ones thePizzaobject is built with no extra getters code inside the class
class PizzaBuilder
Ups:
- Clean code and concept separation
-
PizzaBuilderonly knows about how to build aPizza, nothing else - It allows to build a
Pizzaobject using specific methods assetToppings(for a fine customisation) or directly accessing the attributes by the cascade notation mode without the verbose setters & getters code - Having it as external class allows to reuse a
PizzaBuilderinstance for building more than onePizzaobject - It combines the Builder pattern standards with the fast and flexible Dart cascade notation technique
/// test.dart
print('___PIZZA BBQ___');
Pizza pizza = (
PizzaBuilder('bbq')
..setToppings(['tomato', 'cheese', 'chicken'])
..hasExtraCheese = true
).build();
print(pizza.sauce); // bbq
print(pizza.toppings); // [onion, cheese, chicken]
print(pizza.hasExtraCheese); // true
Pizza BBQ was ordered with a nice bbq sauce, great toppings and an amazing extra of cheese.
The PizzaBuilder uses the cascade notation pattern to build the Pizza:
- uses the customise
setToppingsmethod which verify the needed toppings are part of the included ones - set the
hasExtraCheeseaccessing directly the attribute, because there is no need to create a verbose setter method
Fast, easy and flexible 🍕!
print('___PIZZA Carbonara___');
Pizza pizza2 = (
PizzaBuilder('cream')
..hasExtraCheese = true
).build();
print(pizza2.sauce); // cream
print(pizza2.toppings); // null
print(pizza2.hasExtraCheese); // true
This Pizza Carbonara is a bit weird, don't you think it?... the employee forgot to add toppings to it!
...but for the coding world everything is good, nothing breaks, you can build your Pizza as you want!
print('___PIZZA Margherita___');
Pizza pizza3 = (
PizzaBuilder('olive-oil')
..setToppings(['PINEAPPLE', 'tomato'])
..hasExtraCheese = false
).build();
// Uncaught Error: Really, without cheese? :(
print(pizza3.sauce); // No output due to Uncaught Error
print(pizza3.toppings); // No output due to Uncaught Error
print(pizza3.hasExtraCheese); // No output due to Uncaught Error
Hell yeah, the amazing and famous Neapolitan pizza Margherita has been ordered!
Melted Mozzarella cheese... wait! what? Oh, God! the employee has added PINEAPPLE as topping instead of cheese!
...but building the Pizza by cascade notation pattern using a custom setToppings method allowed the restaurant to detect that the employee made a mistake showing Really, without cheese? :( on the system
Conclusion
The employee always said he was from Naples and the real Pizza was made with pineapple... :)
As I tried to explained above, combining different techniques and patterns such as Builder one along with the OOP standards and the cascade notation from Dart, we have been able to build and easy, flexible and powerful Dart Builder pattern.
...pineapple out! cheese forever!
Top comments (1)
Thanks for your good article. @inakiarroyo
How about named and optional parameters in Dart?