Mẫu Builder
Thuộc phần Lập trình hướng đối tượng trong hành trình Kotlin của Coddy. Bài học 48 trên 57.
Mẫu builder tạo một đối tượng từng bước và kiểm tra toàn bộ đối tượng ở cuối. Builder thu thập các phần bằng những phương thức có thể nối tiếp nhau và trả về chính builder, còn build() tạo đối tượng hoàn chỉnh:
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)
}
}Bên trong main:
val mail = EmailBuilder().to("ada@x.io").to("bo@x.io").subject("Lunch").build()
println(mail)Đầu ra:
Email(to=[ada@x.io, bo@x.io], subject=Lunch)apply rút ngắn mọi bước: nó chạy một khối lệnh với đối tượng làm this và trả về đối tượng đó, nên mỗi bước chỉ cần một dòng. check(condition) { message } ném ra IllegalStateException, tương tự như require nhưng dùng để kiểm tra trạng thái của đối tượng:
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
}
}Bên trong main:
val request = RequestBuilder().apply {
url = "/users"
method = "POST"
header("Accept", "json")
}.build()
println(request)Đầu ra:
POST /users [Accept: json]Một hàm nhận lambda có receiver với kiểu Builder.() -> Unit biến builder thành một DSL nhỏ (ngôn ngữ dành riêng cho miền): bên trong dấu ngoặc nhọn, builder là this, nên người gọi chỉ cần viết cấu hình:
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()Bên trong main:
val lunch = menu("Lunch") {
item("soup", 450)
item("salad", 600)
}
println(lunch)Đầu ra:
== Lunch ==
soup 4.50
salad 6.00Đối số được đặt tên và đối số mặc định của Kotlin đã đáp ứng các trường hợp đơn giản: Pizza(size = 30, cheese = true) đặt tên cho từng lựa chọn ngay tại vị trí gọi. Builder phát huy tác dụng khi bạn tạo đối tượng qua nhiều bước, thu thập danh sách hoặc cần kiểm tra nhiều thuộc tính trước khi đối tượng có thể tồn tại:
data class Pizza(val size: Int = 26, val cheese: Boolean = true, val olives: Boolean = false)
val simple = Pizza(size = 30, olives = true) // không cần bộ dựng
val mail = EmailBuilder() // bộ dựng để tạo từng bước
.to("ada@x.io")
.to("bo@x.io") // tạo một danh sách
.subject("Plan")
.build() // kiểm tra mọi thứ ở bước cuốiThử thách
DễViết một trình dựng cho lớp dữ liệu Email được cung cấp. EmailBuilder có các bước có thể gọi nối tiếp: to(address) (có thể gọi nhiều lần để thêm người nhận), subject(text), body(text) và urgent(). build() trả về một Email hoặc ném ra IllegalStateException với no recipients hoặc no subject (theo đúng thứ tự kiểm tra này). Email khẩn cấp sẽ có [URGENT] ở trước tiêu đề.
Code được cung cấp đọc các lệnh (to ada@x.io, subject Hello, body text, urgent, send), bắt đầu một trình dựng mới sau mỗi dòng --- và in ra email đã tạo, To: ada@x.io, bo@x.io | [URGENT] Hello | text, hoặc error: no subject.
Code của bạn sẽ nằm trong EmailBuilder.kt và Email.kt. Main.kt chứa code đầu vào/đầu ra được cung cấp và bạn không thể chỉnh sửa tệp này.
Tự làm thử
fun main() {
// Đoạn code đầu vào/đầu ra có sẵn: giữ nguyên
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))
}
}
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Lớp và đối tượng
Làm việc với tệpĐịnh nghĩa lớpHàm khởi tạo chínhKhối init và kiểm tra tính hợp lệHàm khởi tạo phụÔn tập: thẻ thư viện4Kế thừa
Lớp mở và lớp conGhi đè thành viênsuper và hàm khởi tạoLớp trừu tượngÔn tập: Danh bạ nhân viên2Thuộc tính và tính đóng gói
Trình truy cập tùy chỉnhBộ sửa đổi phạm vi truy cậpSetter riêng tưlazy và lateinitÔn tập: tài khoản ngân hàng5Giao diện
Khai báo giao diệnPhương thức mặc địnhNhiều giao diệnGiao diện như kiểu dữ liệuÔn tập: Phương thức thanh toánTự luyện tập: Playground Kotlin