I was re-reading Effective Java (it is a rainy Sunday afternoon in Vancouver, and if you are stuck at home, and you happen to be European, and therefore you don’t really care about what Americans call “football”, re-reading Effective Java is not as bad as it sounds) when I soon stumbled upon an example that I think highlights the elegance of Swift (and, to be completely fair, also Kotlin’s).
Effective Java’s Item #2 says: Consider a builder when faced with many constructor parameters. Which makes total sense, if you ask me.
I am not going to discuss or justify the Builder Pattern now, but let’s just assume that it is more than justified.
The canonical implementation of the pattern, and I am going to take the liberty of using the code sample used in Effective Java without any modification, would read like the following (servings and servingSize are the only mandatory parameters, the rest are all optional)
public class NutritionFacts { private final int servingSize; //required private final int servings; //required private final int calories; //optional private final int fat; //optional private final int sodium; //optional private final int carbs; //optional public static class Builder { //Required private final int servingSize; private final int servings; //Optional private int calories = 0; private int fat = 0; private int sodium = 0; private int carbs = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public Builder carbs(int val) { carbs = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbs = builder.carbs; } } class Untitled { public static void main(String[] args) { NutritionFacts cocaCola = new NutritionFacts.Builder(240,8) .calories(100) .sodium(35) .carbs(27) .build(); } }
If we follow the pattern to the letter, it translates into Swift like this:
final class NutritionFacts { private let servingSize: Int private let servings: Int private let calories: Int private let fat: Int private let sodium: Int private let carbs: Int init(builder: Builder) { servingSize = builder.servingSize servings = builder.servings calories = builder.calories fat = builder.fat sodium = builder.sodium carbs = builder.carbs } class Builder { let servingSize: Int let servings: Int private(set) var calories = 0 private(set) var fat = 0 private(set) var carbs = 0 private(set) var sodium = 0 init(servingSize: Int, servings: Int) { self.servingSize = servingSize self.servings = servings } func calories(value: Int) -> Builder { calories = value return self } func fat(value: Int) -> Builder { fat = value return self } func carbs(value: Int) -> Builder { carbs = value return self } func sodium(value: Int) -> Builder { sodium = value return self } func build() -> NutritionFacts { return NutritionFacts(builder: self) } } } let facts = NutritionFacts.Builder(servingSize: 10, servings: 1) .calories(value: 20) .carbs(value: 2) .fat(value: 5) .build()
Which is a lot of boilerplate, no matter how you look at it.
We have discussed Swift’s Default Parameters before in this blog, but in the context of Dependency Injection, in particular when focusing on testability.
But the thing is that Default Parameters, like many other Swift artifacts (or constructs, or whatever is the right word to refer to “stuff that Swift provides and makes your life easier”) can make code much more concise, readable and easier to understand and reason about while removing some of the bloating that certain languages (I’m looking at you, Java) are known for.
Because when providing default values to all the optional parameters, this is how our NutritionFacts look.
final class NutritionFacts { private let servingSize: Int private let servings: Int private let calories: Int private let fat: Int private let sodium: Int private let carbs: Int init(servingSize: Int, servings: Int, calories: Int = 0, fat: Int = 0, sodium: Int = 0, carbs: Int = 0) { self.servingSize = servingSize self.servings = servings self.calories = calories self.fat = fat self.sodium = sodium self.carbs = carbs } } let facts = NutritionFacts(servingSize: 10, servings: 1, calories: 500) let otherFacts = NutritionFacts(servingSize: 10, servings: 1, calories: 1500, sodium: 2)
Much better, eh?
Better for this use case. But Builder main use case is to separate the “parameter recollection” from the building point. You can pass around a Builder, adding data on each phase and finally call build() to get a nice new object. This is not posible with the initializer with optional parameters approach. So, yes it is nicer, but for me, you are solving other problem 🙂