DEV Community

Cover image for Avoiding the Builder Design Pattern in Kotlin
Christian Vasquez
Christian Vasquez

Posted on

Avoiding the Builder Design Pattern in Kotlin

The Builder Design Pattern provides us with a series of methods that can act as an aid in order to let the consumer of your class better understand what is happening under the hood.

But it is considered an Anti-Pattern by some developers.

Why use Builder Design Pattern?

It's an alternative to using multiple constructors by overloading them with more and more parameters.

What are the pros?

  • There are tools that allow you to get hints of what the name of the parameter is, which may help if their names are informative, but it is not always the case. Like, for example: reading code on GitHub or another IDE/editor.
  • You don't need to have all the data required to pass it to your object right when you initialize it.

What are the cons?

  • You can end up having methods that require others to be ran in a certain order; otherwise, the consumer will run into issues if you implement it wrong.
  • The chain of methods can get really long depending on the implementation.
  • The consumer may forget to finish up the statement with the build() method and not get the results they expected.
  • Uses more memory resources.

How can we avoid it?

Default Parameters

Kotlin supports default parameters (Which is also available in other languages like C# and JavaScript).

fun pow(base: Int, power: Int = 2): Int {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

This can work as an alternative to method overloading, since the consumer of this method can use it like this:

pow(2) // outputs 4
pow(2, 3) // outputs 8
Enter fullscreen mode Exit fullscreen mode

Which can make our life easier by only having to maintain a single method.

Named Arguments

This allows our consumers to not only type in exactly what argument they want to assign to an exact parameter, but we can also reorder them in whatever way we want. This can be handy when dealing with "legacy code" that can be hard to understand because of how many parameters requires.

pow(base = 4, power = 3) // outputs 64
pow(power = 3, base = 4) // also outputs 64
Enter fullscreen mode Exit fullscreen mode

Can we combine them?

Of course, my dear Watson!

pow(base = 3) // outputs 9
Enter fullscreen mode Exit fullscreen mode

In the example above we are passing the base argument as a named argument and the power is using the default parameter value in our method signature.

Now that we know how to do this, we can use them to avoid the Builder Design Pattern in Kotlin.

First, let's check out the code we would have to write in Java by creating a simplified version of a Hamburger class:

// Hamburger.java


public class HamburgerJava {
    private final boolean hasKetchup;
    private final boolean hasTomatoes;
    private final int meats;


    private HamburgerJava(Builder builder) {
        hasKetchup = builder.hasKetchup;
        hasTomatoes = builder.hasTomatoes;
        meats = builder.meats;
    }

    public static class Builder {
        private boolean hasKetchup;
        private boolean hasTomatoes;
        private int meats;

        public Builder(int meats) {
            if (meats > 3)
                throw new IllegalArgumentException("Cannot order hamburger with more than 3 meats");
            if (meats < 1)
                throw new IllegalArgumentException("A hamburger must have at least 1 meat.");

            this.meats = meats;
        }

        public Builder addKetchup(boolean hasKetchup) {
            this.hasKetchup = hasKetchup;
            return this;
        }

        public Builder addTomatoes(boolean hasTomatoes) {
            this.hasTomatoes = hasTomatoes;
            return this;
        }

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

    }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's see how we can use our HamburgerJava class:

HamburgerJava doubleMeatWithEverything = new HamburgerJava.Builder(2)
                .addKetchup(true)
                .addTomatoes(true)
                .build();
Enter fullscreen mode Exit fullscreen mode

Cool, maybe some of you guys are used to this.

Now let's take a look at the Kotlin implementation in our Hamburger class:

// Hamburger.kt

class Hamburger(
        val meats: Int,
        val hasKetchup: Boolean = false, 
        val hasTomatoes: Boolean = false
)
Enter fullscreen mode Exit fullscreen mode

Let's see how it looks when we try to use it:

val doubleMeatWithEverything = Hamburger(
            meats = 2,
            hasKetchup = true,
            hasTomatoes = true
    )
Enter fullscreen mode Exit fullscreen mode

By using named arguments and default parameters we can avoid the problem of not knowing what values we are being passed to each parameter and have only a single constructor to maintain.

The only cons we have with this approach is that we lose the ability to pass in values to our object after it's creation.

Which is something I don't think is that common, or is it? 🤔

Discussion (4)

Collapse
sake_92 profile image
Sakib Hadžiavdić

I like named parameters, they are convenient and quick to write. IMHO, a more readable approach are so called "withers", like ones from Immutables library. You can type "with" and have autocomplete suggestion for free. :)

Also, there are some problems if you care about binary compatibility when changing case classes in Scala, see here. IDK about Kotlin though.

Collapse
eugeniyk profile image
eugeniyk • Edited on

@chrisvasqm what about binary compatibility? What's happened with Scala case classes, they are convenient and all, but they are breaking binary compatibility if you'll add one more parameter. How it's resolved in Kotlin?

UPD: exactly what @Sakib Hadžiavdić mentioned

Collapse
jffiorillo profile image
jffiorillo

You can also use this github.com/jffiorillo/jvmbuilder Annotation Processor to generate the builder class automatically for you.

Collapse
paveltrufi profile image
Pavel Razgovorov

Another approach, more similar to what the Builder pattern does, would be the apply function: gist.github.com/AkshayChordiya/27c...