Differences of the builder pattern in Java and Python
In this post, we explore the differences in implementing the builder pattern in Java and Python.
1. Introduction
The builder design pattern 1, first introduced in the "Design Patterns: Elements of Reusable Object-Oriented Software" (1994) 2, popularly known as Gang of Four (GoF), is classified as a creational design pattern. This post focuses on its application in Java and Python.
At my current job we are using different programming languages. We decided to use the buildern pattern for one of our python microservices. While writing the buildern pattern in python, the team brought their "java style" to the python code. However, there is a pythonic way to write it and save some lines of code. This is the story behind this post.
2. Language features in Java and Python
Let's first examine the features of each language that we will later use to implement the design patterns in Python and Java. Python supports function default arguments 3 and named parameters 4, while Java supports function overloading 5.
The following examples demonstrates default arguments:
# Function with default arguments def calculate_area(length, width=5): area = length * width print(f"Area: {area}") # Using the function with both parameters calculate_area(8, 4) # Using the function with the default value for 'width' calculate_area(10) # Width defaults to 5 # You can still explicitly provide a value for 'width' calculate_area(6, 3)
The following Python example demonstrates both default arguments and named parameters:
# Function with named parameters def print_user_info(name, age, city="Unknown", country="Unknown"): print(f"Name: {name}, Age: {age}, City: {city}, Country: {country}") # Using named parameters print_user_info(name="John", age=25, city="New York", country="USA") # Omitting some named parameters (using defaults) print_user_info(name="Alice", age=30) # Mixing ordered and named parameters print_user_info("Bob", 28, country="Canada")
The following Java example shows how function overloading works:
// Java program to demonstrate working of method overloading in Java public class Person { private String firstName; private String lastName; // Person constructor with two arguments, the first and last name. public Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } // Another constructor, this time with a single argument, the first name. public Person(String firstName) { this.firstName = firstName; } public static void main(String[] args) { // Using constructor with two parameters Person person1 = new Person("John", "Doe"); // Using constructor with one parameter Person person2 = new Person("John"); } }
3. The builder pattern in Java 6
Factories and constructors share a limitation: they do not scale well to large numbers of optional parameters. Consider the case of a class representing the ingredients of a Pizza. Most ingredients have nonzero values for only a few of these optional fields.
What sort of constructors or static factories should you write for such a class? Traditionally, programmers have used the telescoping constructor pattern, in which you provide a constructor with only the required parameters, another with a single optional parameter, a third with two optional parameters, and so on, culminating in a constructor with all the optional parameters. Here’s how it looks in practice. For brevity’s sake, only three optional cheese fields are shown:
1: public class Pizza { 2: private final int dough; // 100g, 200g, 300g required 3: private final int mozarella; // 100g, 200g, 300g optional 4: private final int parmesan; // 50g, 100g, 150g optional 5: private final int gorgonzola; // 50g, 100g, 150g optional 6: 7: public Pizza(int dough) { 8: this(dough, 0, 0, 0); 9: } 10: 11: public Pizza(int dough, int mozarella) { 12: this(dough, mozarella, 0, 0); 13: } 14: 15: public Pizza(int dough, int mozarella, int parmesan) { 16: this(dough, mozarella, parmesan, 0); 17: } 18: 19: public Pizza(int dough, int mozarella, int parmesan, int gorgonzola) { 20: this.dough = dough; 21: this.mozarella = mozarella; 22: this.parmesan = parmesan; 23: this.gorgonzola = gorgonzola; 24: } 25: }
When you want to create an instance, you use the constructor with the shortest parameter list containing all the parameters you want to set:
Pizza margarita = new Pizza(200, 200);
In short, the telescoping constructor pattern works, but it is hard to write client code when there are many parameters, and harder still to read it.
Luckily, the builder pattern helps us with the readability and tediousness of the code. Instead of making the desired object directly, the client calls a constructor with all of the required parameters and gets a builder object. Then the client calls setter-like methods on the builder object to set each optional parameter of interest. Finally, the client calls a parameterless build method to generate the object, which is typically immutable. The builder is typically a static member class of the class it builds. Here’s how it looks in practice:
public class Pizza { private final int dough; // 100g, 200g, 300g required private final int mozarella; // 100g, 200g, 300g optional private final int parmesan; // 50g, 100g, 150g optional private final int gorgonzola; // 50g, 100g, 150g optional public static class Builder { // Required parameters private final int dough; // Optional parameters - initialized to default values private int mozarella = 0; private int parmesan = 0; private int gorgonzola = 0; public Builder(int dough) { this.dough = dough; } public Builder setMozarella(int val) { mozarella = val; return this; } public Pizza build() { return new Pizza(this); } } private Pizza(Builder builder) { dough = builder.dough; mozarella = builder.mozarella; parmesan = builder.parmesan; gorgonzola = builder.gorgonzola; } }
The Pizza class is immutable, and all parameter default values are in one place. The builder’s setter methods return the builder itself so that invocations can be chained. Here’s how the client code looks:
Pizza pizza = new Pizza.Builder(200).setMozarella(200).setGorgonzola(50).build();
The Builder pattern simulates default arguments and named parameters as found in Python and eludes the telescoping pattern avoiding function overloading.
4. The builder pattern in Python
In python, we just simply leverage the language support for named parameters and default values as explained in 2 to write pythonic code for the builder pattern.
class Pizza: """ Pizza class to represent a pizza with its ingredients. To set the ingredients the builder pattern is used. """ def __init__( self, dough: int, mozarella: int = 0, parmesan: int = 0, gorgonzola: int = 0, ) -> None: self.dough = dough self.mozarella = mozarella self.parmesan = parmesan self.gorgonzola = gorgonzola
This time we do not need to concatenate calls, nor call a build method to instantiate a pizza object.
pizza = Pizza(200, mozarella=200, gorgonzola=50)
5. Summary
Exploring the Builder Pattern in Java and Python, we uncovered language-specific nuances. While Java employs an inner builder class to simulate features like named parameters and default arguments found natively in Python, the latter provides a more concise and idiomatic approach. The post contrasts these implementations, offering insights into the divergent paths each language takes when applying the Builder Pattern.
Footnotes:
Based on the excellent book "Effective Java: Programming Language Guide" (Third edition 2017) from Joshua Bloch. Item 2: Consider a builder when faced with many constructor paramters.