Builder

The Builder pattern is useful when we need to initialize many fields during new object creation. According to this pattern, instead of using a constructor with a lot of parameters, we can create a nested class that collects all passed arguments and constructs a new object. Let's look at the typical implementation of this pattern in a classic Java way. Let's imagine that we need to cook a burger. For this, we need to define classes of ingredients:

class Meat
class Cheese
class Ketchup
class Bun

We also need to define the Burger class, which encapsulates all ingredients:

class Burger {
private val meat: Meat
private val cheese: Cheese
private val ketchup: Ketchup
private val topBun: Bun
private val bottomBun: Bun

private constructor(meat: Meat, cheese: Cheese, ketchup: Ketchup, topBun: Bun, bottomBun: Bun) {
this.meat = meat
this.cheese = cheese
this.ketchup = ketchup
this.topBun = topBun
this.bottomBun = bottomBun
}
}

The Builder pattern assumes that we use the nested Builder class that obtains all arguments and creates a new instance. This class can be written as follows:

class Builder {
private var meat: Meat = Meat()
private var cheese: Cheese = Cheese()
private var ketchup: Ketchup = Ketchup()
private var topBun: Bun = Bun()
private var bottomBun: Bun = Bun()

fun setMeat(meat: Meat): Builder {
this.meat = meat
return this
}

///..............

fun setBottomBun(bottomBun: Bun): Builder {
this.bottomBun = bottomBun
return this
}

fun build(): Burger {
return Burger(meat, cheese, ketchup, topBun, bottomBun)
}

We can use this implementation as follows:

fun main(args: Array<String>) {
val burger: Burger = Burger.Builder()
.setMeat(Meat())
.setKetchup(Ketchup())
.build()
}

You don't have to use all setters of the Builder class because fields have already been initialized by default values.

As you can see, a classic implementation of this pattern requires a lot of boilerplate code, but in Kotlin we can use named and default argument features, for instance. Let's create the Kotlinger class:

class Kotlinger(private val meat: Meat = Meat(),
private val cheese: Cheese = Cheese(),
private val ketchup: Ketchup = Ketchup(),
private val topBun: Bun = Bun(),
private val bottomBun: Bun = Bun())

 You can use this as follows:

val kotlinger: Kotlinger = Kotlinger(
meat = Meat(),
ketchup = Ketchup()
)

Using the named and default argument features, we have the same benefits as when we use classical implementation.

For more complex cases, we can use Type-Safe Builders. This concept is based on the function with the receiver object feature of Kotlin and brings the power of domain-specific languages (DSLs) to the Builder pattern. Let's consider the following simplified example of user interface creation:

class Window(init: Window.() -> Unit) {
private var header: TextView? = null
private var footer: TextView? = null

init {
init()
}

fun header(init: TextView.() -> Unit) {
this.header = TextView().apply { init() }
}

fun footer(init: TextView.() -> Unit) {
this.footer = TextView().apply { init() }
}
}

The Window class uses the TextView class, which looks as follows:

class TextView {
var text: String = ""
var color: String = "#000000"
}

To make creating a new object of the Window class more understandable, we can create the following window function:

fun window(init: Window.() -> Unit): Window {
return Window(init)
}

Finally, we can create a new instance of the Window class with complex initialization, like this:

window {
header {
text = "Header"
color = "#00FF00"
}
footer {
text = "Footer"
}
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.28.107