Let's Code Kotlin: Goodbye Builders, Hello Data Classes
Long gone are the days of Builder classes and their massive amount of boilerplate code. In this new era, Kotlin has given us Data Classes and I've never looked back. (Except to write this post, and let me tell you: the past was a scary place.)
If it wasn't for auto-generation, I can't say I'd be a good programmer. Back when Java was my sole language used for Android apps, manually writing each and every getter and setter would have been the bane of my existence, and eventually my downfall. Potentially constructors, too. Android Studio luckily can generate them all for me, which is why I'm still alive today.
When I switched to Kotlin, I was amazed. So many things I had been creating for years with too so* many lines of code were now being reduced to a small handful for the necessities.
Let's talk about Data Classes.
The Java Way
In Java, you need to be verbose.
class Pizza {
private Dough dough;
private Sauce sauce;
private Cheese cheese;
private List<Topping> toppings = new ArrayList<>();
private Pizza(Dough dough, Sauce sauce, Cheese cheese, List<Topping> toppings) {
this.dough = dough;
this.sauce = sauce;
this.cheese = cheese;
this.toppings = toppings;
}
static class Builder {
private Dough dough;
private Sauce sauce;
private Cheese cheese;
private List<Topping> toppings = new ArrayList<>();
public Builder() {
}
public Builder(Pizza pizza) {
this.dough = pizza.dough;
this.sauce = pizza.sauce;
this.cheese = pizza.cheese;
this.toppings = pizza.toppings;
}
public Builder setDough(Dough dough) {
this.dough = dough;
return this;
}
public Builder setSauce(Sauce sauce) {
this.sauce = sauce;
return this;
}
public Builder setCheese(Cheese cheese) {
this.cheese = cheese;
return this;
}
public Builder setToppings(List<Topping> toppings) {
this.toppings = toppings;
return this;
}
public Pizza build() {
return new Pizza(dough, sauce, cheese, toppings);
}
}
}
I never want to see that type of code again in my life. To build a pizza, we would do this:
Pizza newOrder = new Pizza.Builder()
.setDough(Dough.HAND_TOSSED)
.setSauce(Sauce.EXTRA_NORMAL)
.setCheese(Cheese.MOZZARELLA)
.setToppings(Arrays.asList(Topping.BACON, Topping.JALEPENOS))
.build();
All of that work to get something, admittedly, pretty. I can't really argue with the results: the builder pattern produces some nice looking code. It's been worth it in the past, but the amount of boilerplate code has always frustrated me.
If I wanted to add one more configuration to it — let's say we split up toppings to be Meats
and Vegetables
— then I have to:
- Change
private List<Topping> toppings = new ArrayList<>();
in the class to reflect the split - Change the private constructor to handle the split
- Duplicate the
toppings
split in the Builder - Remove
setToppings()
in the builder - Add two more
set
s to the builder to handle meats and vegetables.
Heaven forbid I want to add uncookedToppings
to the configuration.
It's entirely too much and my brain hurts just thinking about it.
The Kotlin Way
In Kotlin, there's a much simpler way to handle builders: not having one at all.
data class Pizza(val dough: Dough,
val sauce: Sauce,
val cheese: Cheese,
val toppings: List<Topping>) {
}
That entire Java class gets reduced to 5 lines; it could be 1 but this how I like to format my classes.
The code to make a pizza now turns into this:
val newOrder = Pizza(
Dough.HAND_TOSSED,
Sauce.EXTRA_NORMAL,
Cheese.MOZZARELLA,
listOf(Topping.BACON, Topping.JALEPENOS)
)
Again, this could all end up on one line, but this is how I format my object initializations if three or more parameters being provided.
To be fair, you can do this in Java as well by removing the builder and allowing you to use the public constructor. And for simple cases, sure, that could be fine. But then if you have defaults for each value, you'd probably end up in a case where you have a million constructors letting you configure the pizza however you want: just set the dough and cheese, or just the sauce and dough. At this point, the Builder provides you with more flexibility by allowing you to set the defaults there, and if they aren't provided with one of the setters, it'll fallback to the default.
But then we're back to using the Builder, and that's where my frustration comes in.
In Kotlin, you can provide the default value, too:
data class Pizza(val dough: Dough = Dough.NORMAL,
val sauce: Sauce = Sauce.NORMAL,
val cheese: Cheese = Cheese.MOZZARELLA,
val toppings: List<Topping> = listOf()) {
}
This is your standard pizza: regular dough, regular sauce, mozzarella cheese, and no toppings.
val defaultPizza = Pizza()
Our plain cheese pizza is a simple Pizza()
.
Extra cheese? We can set just that.
val extraCheese = Pizza(cheese = Cheese.EXTRA_MOZZARELLA)
Hand-tossed with mushrooms? Easy, and you can call the toppings first then the dough, using Kotlin's Named Arguments.
val handTossedMushroom = Pizza(toppings = listOf(Topping.MUSHROOMS), dough = Dough.HAND_TOSSED)
.copy
As an added bonus of data classes, they have a built-in .copy()
method. In our Java example, we had to manually make a separate constructor that took in an old Pizza
and set the values it held into our new builder, allowing us to change them.
Pizza updatedOrder = new Pizza.Builder(newOrder)
.setCheese(Cheese.EXTRA_MOZZARELLA)
.build();
In Kotlin, however, it's all baked in. (You get it? Baked? lol) And with the help of Named Arguments, you can only change the values you want to change.
val updatedOrder = newOrder.copy(cheese = Cheese.EXTRA_MOZZARELLA)
Data classes are absolutely amazing. For more information on them, visit the official documentation.
Anyone else love data classes as much as me? Leave a comment below and let me know how your life is better because of them. 😉