Menu
CoddyTech

Mẫu trang trí

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 47 trên 57.

Mẫu decorator bổ sung hành vi cho một đối tượng bằng cách bọc nó trong một đối tượng khác có cùng giao thức. Đối tượng bọc chuyển tiếp lời gọi đến đối tượng được bọc và thêm phần của riêng nó trước hoặc sau:

protocol Coffee {
    func cost() -> Int
    func label() -> String
}

struct Espresso: Coffee {
    func cost() -> Int { 200 }
    func label() -> String { "espresso" }
}

struct WithMilk: Coffee {
    let base: any Coffee
    func cost() -> Int { base.cost() + 50 }
    func label() -> String { base.label() + " + milk" }
}

let drink = WithMilk(base: Espresso())
print(drink.label())
print(drink.cost())

Đầu ra:

espresso + milk
250

Vì một decorator bản thân nó là một Coffee, các decorator có thể bọc lẫn nhau theo mọi cách kết hợp và thứ tự mà không cần tạo một lớp con cho mỗi cách kết hợp:

protocol Coffee { func cost() -> Int; func label() -> String }
struct Espresso: Coffee { func cost() -> Int { 200 }; func label() -> String { "espresso" } }
struct WithMilk: Coffee {
    let base: any Coffee
    func cost() -> Int { base.cost() + 50 }
    func label() -> String { base.label() + " + milk" }
}
struct WithSyrup: Coffee {
    let base: any Coffee
    func cost() -> Int { base.cost() + 30 }
    func label() -> String { base.label() + " + syrup" }
}

let fancy = WithSyrup(base: WithMilk(base: WithMilk(base: Espresso())))
print(fancy.label())
print(fancy.cost())

Đầu ra:

espresso + milk + milk + syrup
330

Decorator phù hợp để bổ sung các chức năng xuyên suốt như ghi nhật ký, lưu bộ nhớ đệm hoặc xác thực quanh một thành phần hiện có mà không làm thay đổi thành phần đó:

protocol DataSource { func fetch(_ id: Int) -> String }
struct Database: DataSource { func fetch(_ id: Int) -> String { "row \(id)" } }

struct Logged: DataSource {
    let inner: any DataSource
    func fetch(_ id: Int) -> String {
        print("fetching \(id)")
        return inner.fetch(id)
    }
}

let source: any DataSource = Logged(inner: Database())
print(source.fetch(7))

Đầu ra:

fetching 7
row 7

Đoạn code sử dụng đối tượng chỉ thấy giao thức, nên không thể phân biệt đối tượng đã được trang trí với đối tượng thông thường. Nhờ vậy, bạn có thể áp dụng mẫu này sau mà không gây rủi ro.

func printReceipt(_ c: any Coffee) {      // Hoạt động với Espresso và mọi phiên bản được bọc
    print("\(c.label()): \(c.cost())")
}
challenge icon

Thử thách

Dễ

Pizza được tạo bằng các decorator. Protocol Pizza có price() và toppings() (một mảng tên). Base có giá 800 và không có topping. Hãy triển khai các decorator Cheese (+150), Olives (+100) và Double (nhân đôi giá của mọi thứ bên trong nó và lặp lại topping hai lần). Hoàn thành build(_:), hàm này bắt đầu từ Base và bọc nó bằng một decorator cho mỗi tên theo đúng thứ tự.

Code được cung cấp đọc các dòng gồm tên topping được phân tách bằng dấu phẩy (hoặc một dòng trống), tạo từng chiếc pizza rồi in ra 1050: cheese, olives hoặc 800: plain.

Đặt code của bạn trong Pizza.swift, Base.swift, Cheese.swift, Olives.swift, Double.swift và Functions.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 đã có sẵn: giữ nguyên
var input: [String] = []
while let line = readLine() { input.append(line) }
for line in input {
    let names = line.isEmpty ? [] : line.split(separator: ",").map(String.init)
    let p = build(names)
    let tops = p.toppings()
    print("\(p.price()): " + (tops.isEmpty ? "plain" : tops.joined(separator: ", ")))
}
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