Menu
CoddyTech

Mẫu xây dựng

Thuộc phần Lập trình hướng đối tượng trong hành trình Swift của Coddy. Bài học 48 trên 57.

Mẫu builder tạo một đối tượng phức tạp theo từng bước. Mỗi bước thiết lập một phần rồi trả về builder, nhờ vậy bạn có thể nối các bước với nhau. Cuối cùng, build() tạo ra đối tượng hoàn chỉnh:

struct Request {
    let url: String
    let method: String
    let headers: [String: String]
}

struct RequestBuilder {
    private var url = ""
    private var method = "GET"
    private var headers: [String: String] = [:]

    func url(_ u: String) -> RequestBuilder { var b = self; b.url = u; return b }
    func method(_ m: String) -> RequestBuilder { var b = self; b.method = m; return b }
    func header(_ k: String, _ v: String) -> RequestBuilder { var b = self; b.headers[k] = v; return b }
    func build() -> Request { Request(url: url, method: method, headers: headers) }
}

let req = RequestBuilder().url("/users").method("POST").header("Auth", "token").build()
print("\(req.method) \(req.url) \(req.headers.count) header")

Đầu ra:

POST /users 1 header

Builder có thể kiểm tra toàn bộ cấu hình trước khi tạo đối tượng, nên một đối tượng không hợp lệ sẽ không bao giờ được tạo. Ở đây, build() ném ra lỗi khi thiếu một phần bắt buộc:

enum BuildError: Error { case missingURL }

struct Request { let url: String; let method: String }

struct RequestBuilder {
    private var url: String?
    private var method = "GET"
    func url(_ u: String) -> RequestBuilder { var b = self; b.url = u; return b }
    func method(_ m: String) -> RequestBuilder { var b = self; b.method = m; return b }
    func build() throws -> Request {
        guard let url else { throw BuildError.missingURL }
        return Request(url: url, method: method)
    }
}

do {
    _ = try RequestBuilder().method("PUT").build()
} catch {
    print("cannot build: \(error)")
}

Đầu ra:

cannot build: missingURL

Mỗi bước sao chép cấu trúc và thay đổi bản sao, vì vậy bạn có thể dùng lại một trình tạo đã được cấu hình một phần làm mẫu cho nhiều đối tượng:

struct PizzaBuilder {
    private var size = "medium"
    private var toppings: [String] = []
    func size(_ s: String) -> PizzaBuilder { var b = self; b.size = s; return b }
    func add(_ t: String) -> PizzaBuilder { var b = self; b.toppings.append(t); return b }
    func build() -> String { "\(size) pizza with " + (toppings.isEmpty ? "nothing" : toppings.joined(separator: ", ")) }
}

let base = PizzaBuilder().size("large").add("cheese")
print(base.add("ham").build())
print(base.add("olives").build())

Đầu ra:

large pizza with cheese, ham
large pizza with cheese, olives

Các giá trị tham số mặc định của Swift đã đáp ứng được những trường hợp đơn giản (Request(url: "/", method: "GET")). Builder hữu ích khi một đối tượng có nhiều phần tùy chọn, các phần phụ thuộc lẫn nhau hoặc cần kiểm tra quy tắc trước khi đối tượng có thể tồn tại.

// Đơn giản: chỉ cần các đối số mặc định (default arguments)
struct Point { var x = 0, y = 0 }
// Phức tạp: nhiều phần tùy chọn và bước kiểm tra tính hợp lệ, nên dùng trình xây dựng (builder)
let report = ReportBuilder().title("Q3").add(section: s1).add(section: s2).footer("draft").build()
challenge icon

Thử thách

Dễ

Viết một trình tạo thông điệp email. EmailBuilder có các bước có thể nối tiếp nhau to(_:) (có thể gọi nhiều lần để thêm người nhận), subject(_:), body(_:) và urgent(). build() trả về một Email hoặc ném ra EmailError.noRecipients hoặc EmailError.noSubject. Email khẩn cấp có [URGENT] ở trước tiêu đề.

Code được cung cấp đọc các lệnh cho từng email, phân tách bằng các dòng --- (to ada@x.io, subject Hello, body text, urgent, rồi đến send) và in ra To: ada@x.io, bo@x.io | [URGENT] Hello | text, error: no recipients hoặc error: no subject.

Code của bạn đặt trong Email.swift, EmailError.swift và EmailBuilder.swift. main.swift 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ử

// Đoạn code đầu vào/đầu ra đã cho: giữ nguyên
var input: [String] = []
while let line = readLine() { input.append(line) }
var builder = EmailBuilder()
for line in input {
    let p = line.split(separator: " ", maxSplits: 1).map(String.init)
    switch p[0] {
    case "to": builder = builder.to(p[1])
    case "subject": builder = builder.subject(p[1])
    case "body": builder = builder.body(p[1])
    case "urgent": builder = builder.urgent()
    case "send":
        do {
            let e = try builder.build()
            print("To: \(e.recipients.joined(separator: ", ")) | \(e.subject) | \(e.body)")
        } catch EmailError.noRecipients {
            print("error: no recipients")
        } catch {
            print("error: no subject")
        }
    default: builder = EmailBuilder()
    }
}
quiz iconTự kiểm tra

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

Tự luyện tập: Playground Swift