Kotlin Design Patterns: Simplifying the Builder Pattern

Originally posted to my blog, at: https://fugisawa.com/kotlin-design-patterns-simplifying-the-builder-pattern/

Published on
5 read
Kotlin Design Patterns: Simplifying the Builder Pattern

The Builder pattern is a design pattern used to construct complex objects step by step. It separates the construction of an object from its representation, allowing the same construction process to create different types.

When creating complex objects, direct construction using constructors might involve many parameters, leading to unclear code and difficult error handling. Also, in languages that does not have features like named parameters and default values, construction through constructors or factory methods leads to lots of overloading.

The Builder pattern solves this by providing a clear, step-by-step approach to object construction. It uses a separate builder class to construct the object. The builder class has methods to set the object's parameters and a method to finalize the construction process.

Traditional Approach in Java:

In Java, a separate Builder class is used. It's usually an inner static class that allows setting various properties step by step and then builds the final Car object.

public class Car {

    private final String make;
    private final String model;
    private final int year;

    // Getters ommited. 
    private Car(Builder builder) {
        this.make = builder.make;
        this.model = builder.model;
        this.year = builder.year;
    }

    public static class Builder {
        private String make;
        private String model;
        private int year;

        public Builder withMake(String make) { 
            this.make = make; 
            return this; 
        }

        public Builder withModel(String model) { 
            this.model = model; 
            return this; 
        }

        public Builder withYear(int year) { 
            this.year = year; 
            return this; 
        }

        public Car build() { return new Car(this); }

    }

}
 

// Usage

Car car1 = new Car.Builder()
        .withMake("Honda")
        .withModel("Civic")
        .withYear(2020)
        .build();

Car car2 = new Car.Builder()
        .withMake("Audi")
        .withModel("RS8")
        .build();

This patterns replaces the need for multiple constructors, like in this example below, and allow for more readable and expressive attribute setting.

public class Car {
    private final String make;
    private final String model;
    private final int year;

    // Getters ommited.
    public Car(String make, String model, int year) { /* Set member fields. */ }
    public Car(String make, String model) { /* Set member fields. */ }
    public Car(String model, int year) { /* Set member fields. */ }
    public Car(String make) { /* Set member fields. */ }
    public Car(int year) { /* Set member fields. */ }
}

Kotlin's Approach:

Kotlin provides named parameters and default arguments, which can make the Builder pattern unnecessary in most cases.

In Kotlin, the data class is used with named parameters and default arguments, making the construction of objects straightforward and clear without a separate builder.

data class Car(
    val make: String = "N/A", 
    val model: String = "N/A", 
    val year: Int? = null,
)

// Usage example:

val car1 = Car(
    make = "Honda", 
    model = "Civic", 
    year = 2024,
)

val car2 = Car(
    make = "Audi",
    model = "RS8",
)

If you need to validate the arguments, you can still use features like init blocks or custom setter logic. Check those examples:

Using init Blocks for Arguments Validation:

data class Car(
    val make: String = "N/A",
    val model: String = "N/A", 
    val year: Int? = null,
) {
    init {
        require(make.isNotEmpty()) { "Make cannot be empty" }
        require(model.isNotEmpty()) { "Model cannot be empty" }
    }
}

// Usage example:

try {
    val car = Car(
        make = "Honda", 
        model = "   ", // This will throw an IllegalArgumentException
    ) 

} catch (e: IllegalArgumentException) {

    println(e.message)

}

Using Custom Setter Logic for Property Validation:

class Car(make: String = "N/A", model: String = "N/A", year: Int?) {
    var make: String = make
        set(value) {
            require(value.isNotEmpty()) { "Make cannot be empty" }
            field = value
        }

    var model: String = model
        set(value) {
            require(value.isNotEmpty()) { "Model cannot be empty" }
            field = value
        }
}

// Usage

val car = Car(make = "Honda", model = "Civic")
car.model = "   " // This will throw an IllegalArgumentException

Setting Object Properties After Instantiation with Kotlin's apply

In some cases, you may want to create an object first and then set its properties later. Kotlin provides a concise and expressive way to achieve this using the apply function. The apply function allows you to execute a block of code on an object and then return the object itself. This is especially useful when you want to set multiple properties of an object after it has been created.

Let's adapt our Car example to demonstrate this approach:

data class Car(var make: String = "N/A", var model: String = "N/A", var year: Int? = null)

// Create a car object without setting properties initially
val car = Car().apply {
    make = "Honda"
    year = 2023
}
// And, of course, you can always do it the traditional way:

car.model = "Civic"

Kotlin's features simplifying Builder Pattern:

  1. Named Arguments: Allow specifying which parameter you are setting, enhancing readability.

  2. Default Arguments: Let you omit some arguments, using default values instead.

  3. Data Classes: Provide a concise way to create classes holding data (getters, settets, toStringequalshashCode, destructuring methods etc. automatically implemented by the compiler).

Final Thougths

Kotlin's features like named parameters and default arguments simplify object construction compared to the traditional Builder pattern. This leads to simpler, concise, more readable and maintainable code.

To explore more about design patterns and other Kotlin-related topics, subscribe to my newsletter on https://fugisawa.com/ and stay tuned for more insights and updates.

Author
Lucas Fugisawa
Lucas Fugisawa

-- Lucas Fugisawa

Discussion (0)

Subscribe