Builder Pattern
Part of the Object Oriented Programming section of Coddy's Kotlin journey. Lesson 48 of 57.
The builder pattern creates an object step by step and checks the whole thing at the end. The builder collects the parts in chainable methods that return the builder itself, and build() creates the finished object:
data class Email(val to: List<String>, val subject: String)
class EmailBuilder {
private val recipients = mutableListOf<String>()
private var subject = ""
fun to(address: String): EmailBuilder {
recipients.add(address)
return this
}
fun subject(text: String): EmailBuilder {
subject = text
return this
}
fun build(): Email {
check(recipients.isNotEmpty()) { "no recipients" }
return Email(recipients.toList(), subject)
}
}Inside main:
val mail = EmailBuilder().to("ada@x.io").to("bo@x.io").subject("Lunch").build()
println(mail)Output:
Email(to=[ada@x.io, bo@x.io], subject=Lunch)apply shortens every step: it runs a block with the object as this and returns the object, so a step becomes one line. check(condition) { message } throws IllegalStateException, the counterpart of require for the object's state:
class RequestBuilder {
var url = ""
var method = "GET"
private val headers = mutableListOf<String>()
fun header(name: String, value: String) = apply { headers.add("$name: $value") }
fun build(): String {
check(url.isNotEmpty()) { "no url" }
return "$method $url " + headers
}
}Inside main:
val request = RequestBuilder().apply {
url = "/users"
method = "POST"
header("Accept", "json")
}.build()
println(request)Output:
POST /users [Accept: json]A function that takes a lambda with receiver, of type Builder.() -> Unit, turns the builder into a small DSL (a domain-specific language): inside the braces, the builder is this, so the caller writes only the configuration:
class MenuBuilder(private val title: String) {
private val items = mutableListOf<String>()
fun item(name: String, cents: Int) {
items.add("$name %d.%02d".format(cents / 100, cents % 100))
}
fun build() = "== $title ==\n" + items.joinToString("\n")
}
fun menu(title: String, block: MenuBuilder.() -> Unit): String = MenuBuilder(title).apply(block).build()Inside main:
val lunch = menu("Lunch") {
item("soup", 450)
item("salad", 600)
}
println(lunch)Output:
== Lunch ==
soup 4.50
salad 6.00Kotlin's named and default arguments already cover simple cases: Pizza(size = 30, cheese = true) names every choice at the call site. A builder pays off when the object is assembled over several steps, collects lists, or needs checks across fields before it may exist:
data class Pizza(val size: Int = 26, val cheese: Boolean = true, val olives: Boolean = false)
val simple = Pizza(size = 30, olives = true) // no builder needed
val mail = EmailBuilder() // a builder for step-by-step assembly
.to("ada@x.io")
.to("bo@x.io") // collects a list
.subject("Plan")
.build() // checks everything at the endChallenge
EasyWrite a builder for the supplied data class Email. EmailBuilder has chainable steps: to(address) (may be called several times and adds recipients), subject(text), body(text) and urgent(). build() returns an Email or throws IllegalStateException with no recipients or no subject (in that order of checks). An urgent email gets [URGENT] in front of its subject.
The supplied code reads commands (to ada@x.io, subject Hello, body text, urgent, send), starts a new builder after each --- line and prints the built email, To: ada@x.io, bo@x.io | [URGENT] Hello | text, or error: no subject.
Your code goes in EmailBuilder.kt and Email.kt. Main.kt holds the supplied input/output code and cannot be edited.
Try it yourself
fun main() {
// Supplied input/output code: keep it as it is
val input = generateSequence(::readLine).toList()
var builder = EmailBuilder()
for (cmd in input) {
when {
cmd == "send" -> {
try {
println(builder.build())
} catch (e: IllegalStateException) {
println("error: ${e.message}")
}
}
cmd == "---" -> builder = EmailBuilder()
cmd == "urgent" -> builder.urgent()
cmd.startsWith("to ") -> builder.to(cmd.drop(3))
cmd.startsWith("subject ") -> builder.subject(cmd.drop(8))
else -> builder.body(cmd.drop(5))
}
}
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Classes and Objects
Working With FilesDefining ClassesPrimary Constructorsinit Blocks and ValidationSecondary ConstructorsRecap - Library Card4Inheritance
Open Classes and SubclassesOverriding Memberssuper and ConstructorsAbstract ClassesRecap - Staff Directory2Properties and Encapsulation
Custom AccessorsVisibility ModifiersPrivate Setterslazy and lateinitRecap - Bank Account5Interfaces
Declaring InterfacesDefault MethodsMultiple InterfacesInterfaces as TypesRecap - Payment Methods8Generics
Generic FunctionsGeneric ClassesType ConstraintsVariance With in and outRecap - Priority Queue11Project: Library Management
Books and MembersBorrowing BooksPractice on your own: Kotlin playground