DEV Community

Lucas Fugisawa
Lucas Fugisawa

Posted on • Edited on • Originally published at fugisawa.com

4

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();
Enter fullscreen mode Exit fullscreen mode

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. */ }
}
Enter fullscreen mode Exit fullscreen mode

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",
)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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, toString, equals, hashCode, 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.

--
This article was originally posted to my Lucas Fugisawa on Kotlin blog, at: https://fugisawa.com/kotlin-design-patterns-simplifying-the-builder-pattern/

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.

Top comments (0)

Great read:

Is it Time to go Back to the Monolith?

History repeats itself. Everything old is new again and I’ve been around long enough to see ideas discarded, rediscovered and return triumphantly to overtake the fad. In recent years SQL has made a tremendous comeback from the dead. We love relational databases all over again. I think the Monolith will have its space odyssey moment again. Microservices and serverless are trends pushed by the cloud vendors, designed to sell us more cloud computing resources.

Microservices make very little sense financially for most use cases. Yes, they can ramp down. But when they scale up, they pay the costs in dividends. The increased observability costs alone line the pockets of the “big cloud” vendors.

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay