Builder Design Pattern in Kotlin

1. Definition

The Builder Design Pattern separates the construction of a complex object from its representation. It allows for the creation of different representations using the same construction process.

In this tutorial, we will learn how to implement a Builder Design Pattern using Kotlin programming language.

2. Problem Statement

Imagine you're developing a restaurant ordering system where customers can customize their burgers. The burger can have various ingredients like lettuce, tomato, cheese, pickles, and many others. Creating a burger with multiple constructor parameters or setters can lead to cumbersome and error-prone client code.

3. Solution

Use the Builder pattern to provide a clear and fluent API for constructing complex objects step by step. This not only makes the client code more readable but also ensures the creation process is controlled and consistent.

4. Real-World Use Cases

1. Building a complex Document Object Model (DOM) for XML and HTML.

2. Creating a detailed user profile with various optional parameters.

5. Implementation Steps

1. Create a "Product" class that represents the complex object to be built.

2. Define a "Builder" interface specifying methods for building parts of the product.

3. Implement the "Builder" interface in a concrete class.

4. Use a "Director" class to construct the product using the builder.

6. Example 1: Implementation in Kotlin Programming

// Product
class Burger(private val ingredients: MutableList<String> = mutableListOf()) {
    fun addIngredient(ingredient: String) {
        ingredients.add(ingredient)
    }
    override fun toString(): String {
        return "Burger(ingredients=$ingredients)"
    }
}
// Builder Interface
interface BurgerBuilder {
    fun addLettuce(): BurgerBuilder
    fun addTomato(): BurgerBuilder
    fun addCheese(): BurgerBuilder
    fun build(): Burger
}
// Concrete Builder
class CustomBurgerBuilder : BurgerBuilder {
    private val burger = Burger()
    override fun addLettuce(): BurgerBuilder {
        burger.addIngredient("Lettuce")
        return this
    }
    override fun addTomato(): BurgerBuilder {
        burger.addIngredient("Tomato")
        return this
    }
    override fun addCheese(): BurgerBuilder {
        burger.addIngredient("Cheese")
        return this
    }
    override fun build(): Burger {
        return burger
    }
}
fun main() {
    // Client code
    val burger = CustomBurgerBuilder()
        .addLettuce()
        .addTomato()
        .addCheese()
        .build()
    println(burger)
}

Output:

Burger(ingredients=[Lettuce, Tomato, Cheese])

Explanation:

1. Burger represents the product being built.

2. The BurgerBuilder interface specifies the methods required to create a burger.

3. CustomBurgerBuilder is a concrete implementation of the builder interface. Each method returns this, allowing for method chaining (fluent interface).

4. The client code creates a burger using the builder, demonstrating how you can create a complex object in a controlled and step-by-step manner.

7. Example 2: Implementation in Kotlin Programming

Problem Statement

When constructing a custom computer system, there are numerous configurations and components to choose from: different processors, memory sizes, storage types, graphics cards, etc. Initializing such a computer system using constructors directly can lead to a very complicated and confusing process.

Solution

Use the Builder pattern to offer a clear and fluent API for step-by-step construction of complex objects. This ensures that client code remains readable and the creation process is consistent.

Implementation Steps

1. Define the "Product" class which is the complex object to be constructed.

2. Create a "Builder" interface that declares methods for constructing the product's parts.

3. Provide a concrete class that implements the Builder interface.

4. Construct the product using the builder.

Implementation in Kotlin Programming

// Product
class ComputerSystem {
    var processor: String = ""
    var memory: String = ""
    var storage: String = ""
    var graphics: String = ""
    override fun toString(): String {
        return "ComputerSystem(processor='$processor', memory='$memory', storage='$storage', graphics='$graphics')"
    }
}
// Builder Interface
interface SystemBuilder {
    fun setProcessor(proc: String): SystemBuilder
    fun setMemory(mem: String): SystemBuilder
    fun setStorage(store: String): SystemBuilder
    fun setGraphics(graph: String): SystemBuilder
    fun build(): ComputerSystem
}
// Concrete Builder
class CustomSystemBuilder : SystemBuilder {
    private val system = ComputerSystem()
    override fun setProcessor(proc: String): SystemBuilder {
        system.processor = proc
        return this
    }
    override fun setMemory(mem: String): SystemBuilder {
        system.memory = mem
        return this
    }
    override fun setStorage(store: String): SystemBuilder {
        system.storage = store
        return this
    }
    override fun setGraphics(graph: String): SystemBuilder {
        system.graphics = graph
        return this
    }
    override fun build(): ComputerSystem {
        return system
    }
}
fun main() {
    // Client code
    val customSystem = CustomSystemBuilder()
        .setProcessor("Intel i9")
        .setMemory("32GB DDR4")
        .setStorage("1TB SSD")
        .setGraphics("NVIDIA RTX 3080")
        .build()
    println(customSystem)
}

Output:

ComputerSystem(processor='Intel i9', memory='32GB DDR4', storage='1TB SSD', graphics='NVIDIA RTX 3080')

Explanation:

1. ComputerSystem represents the complex object we wish to construct.

2. The SystemBuilder interface provides methods to set components for a computer system.

3. CustomSystemBuilder is the concrete builder implementing the methods, returning this for method chaining.

4. The client code creates a custom computer system using the builder, streamlining the creation of a complex object.

8. When to use?

The Builder pattern is useful when:

1. An object needs to be created with many optional components or configurations.

2. The process of constructing an object should be kept separate from its representation.

3. A clear and fluent interface is needed to create an instance of a complex object.

Comments