Menu
CoddyTech

Mẫu ủy quyền

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

Ủy quyền cho phép một đối tượng giao một số quyết định cho một đối tượng khác, gọi là đối tượng ủy quyền, thông qua một giao thức. Đối tượng tự thực hiện công việc của mình và hỏi đối tượng ủy quyền tại những điểm có thể thay đổi. Các framework của Apple sử dụng mẫu này ở khắp nơi:

protocol DownloadDelegate: AnyObject {
    func progress(_ percent: Int)
    func finished(_ file: String)
}

final class Downloader {
    weak var delegate: DownloadDelegate?

    func download(_ file: String) {
        for p in [50, 100] { delegate?.progress(p) }
        delegate?.finished(file)
    }
}

final class Screen: DownloadDelegate {
    func progress(_ percent: Int) { print("\(percent)%") }
    func finished(_ file: String) { print("done: \(file)") }
}

let screen = Screen()
let d = Downloader()
d.delegate = screen
d.download("photo.png")

Đầu ra:

50%
100%
done: photo.png

Thuộc tính delegate không bắt buộc và các lệnh gọi dùng delegate?., vì vậy đối tượng vẫn hoạt động ngay cả khi không có ai lắng nghe. Thuộc tính này được khai báo là weak để đối tượng không tự giữ delegate tồn tại, nhờ đó tránh các vòng tham chiếu giữ lại giữa những đối tượng trỏ đến nhau:

weak var delegate: DownloadDelegate?   // yêu cầu giao thức chỉ áp dụng cho lớp: AnyObject
delegate?.finished(file)               // không làm gì khi không có delegate

Một delegate cũng có thể trả lời các câu hỏi, nhờ đó tùy chỉnh hành vi của đối tượng mà không cần tạo lớp con:

protocol ListDelegate: AnyObject {
    func shouldShow(_ item: String) -> Bool
}

final class ListView {
    weak var delegate: ListDelegate?
    func render(_ items: [String]) -> [String] {
        items.filter { delegate?.shouldShow($0) ?? true }
    }
}

final class HideDrafts: ListDelegate {
    func shouldShow(_ item: String) -> Bool { !item.hasPrefix("draft") }
}

let view = ListView()
let rules = HideDrafts()
print(view.render(["a", "draft b"]))
view.delegate = rules
print(view.render(["a", "draft b"]))

Đầu ra:

["a", "draft b"]
["a"]

Ủy quyền khác với mẫu observer: một đối tượng có một delegate để hỏi và thông báo, còn một đối tượng chủ thể có thể có nhiều observer mà nó chỉ thông báo.

// Ủy quyền: một bên phối hợp, có thể trả lời câu hỏi
weak var delegate: TableDelegate?
// Quan sát: số lượng bên lắng nghe không giới hạn, chỉ nhận thông báo
private var observers: [(Event) -> Void] = []
challenge icon

Thử thách

Dễ

Checkout yêu cầu đối tượng ủy quyền đưa ra quyết định. Giao thức CheckoutDelegate (chỉ dành cho lớp) có discount(for total: Int) -> Int và didComplete(total: Int). Checkout có một weak var delegate, còn pay(_ prices: [Int]) -> Int tính tổng giá, trừ đi khoản giảm giá của đối tượng ủy quyền (bằng 0 nếu không có đối tượng ủy quyền), thông báo tổng cuối cùng cho đối tượng ủy quyền và trả về tổng đó. Hãy triển khai LoyaltyShop, một đối tượng ủy quyền giảm giá 10% cho tổng từ 100 trở lên và ghi lại mọi tổng đã hoàn tất vào receipts.

Code được cung cấp đọc các dòng chứa giá, phân tách bằng dấu phẩy. Chương trình thanh toán dòng đầu tiên mà không có đối tượng ủy quyền, các dòng còn lại với một LoyaltyShop, in ra từng tổng và cuối cùng là receipts: 120 90.

Code của bạn nằm trong CheckoutDelegate.swift, Checkout.swift và LoyaltyShop.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) }
let checkout = Checkout()
let shop = LoyaltyShop()
for (i, line) in input.enumerated() {
    if i == 1 { checkout.delegate = shop }
    print(checkout.pay(line.split(separator: ",").map { Int($0)! }))
}
print("receipts: " + shop.receipts.map { String($0) }.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