Contents
Chào bạn đến với Fx Studio. Sau khi, bạn đã đi qua nhiều bài về 2 thế lực Protocol & Closure rồi. Thì bài viết này sẽ giúp bạn tham khảo cách để lựa chọn cái gì phù hợp trong từng trường hợp. Chủ đề bài viết lần này là Delegates vs. Closure Callback.
Bài viết này mình tham khảo và dịch lại từ bài viết gốc sau đây:
Dành cho các bạn chưa kịp tìm hiểu, thì mình đã có nhiều bài viết về Protocol vs. Closure. Bạn có thể tham khảo sau đây:
Cũng khá nhiều kiến thức được liệt kê ở trên rồi. Bây giờ thì …
Bắt đầu thôi!
1. Callback base pattern
Ngày xửa ngày xưa, khi dev iOS còn sử dụng Objective-C để làm nên những ứng dụng tuyệt vời cho iPhone. Lúc đó, Delegate Pattern được xem là thần thánh.
Với nó, bạn có thể áp dụng cho nhiều mô hình lập trình thời thượng lúc ấy (MVC, MVVM …). Bản thân trong chính nền tảng của mình với Cocoa Framework, thì Protocol & Delegate xuất hiện ở mọi nơi. Bạn có thể bắt gặp nó trong nhiều class/struct của hệ thống. Ví dụ như: UITableViewDelegate, UIPickerViewDelegate, MKMapViewDelegate.
Nhưng lúc đó, đã có sự manh nha của một thế lực khác. Đó là code bock
, người tiền nhiệm của Closure sau này.
// define void (^simpleBlock)(void) = ^{ NSLog(@"This is a block"); }; // call simpleBlock();
Mãi tới khi Swift ra đời, thực sự lúc này nó đã tiến hoá. Và âm thầm thay thế vai trò của Delegate trong tất cả các class/struct. Người ta gọi nó là:
The Closure Callback Pattern
Sau đây, mình sẽ đưa ra ví dụ code về 2 mẫu design này. Để giúp bạn có sự so sánh và hồi tưởng.
- Delegate Pattern
protocol ImageDownloaderDelegate: class { func imageDownloader(_ downloader: ImageDownloader, didDownloadImage image: UIImage) } class ImageDownloader { weak var delegate: ImageDownloaderDelegate? func downloadImage(url: URL) { // download the image asynchronously then... delegate?.imageDownloader(self, didDownloadImage: theImage) } }
- Closure Pattern
class ImageDownloader { var didDownload: ((UIImage?) -> Void)? func downloadImage(url: URL) { // download the image asynchronously then... didDownload?(theImage) } }
2. Vấn đề
Tiếp theo, chúng ta sẽ đi vào những vấn đề được xem là thường ngày khi bạn code iOS. Đối với mỗi cái, bạn có thể so sánh giữa Delegate & Closure Callback như thế nào.
2.1. Breaking the retain cycle
Đây là câu chuyện muôn thuở trong iOS hay bất cứ nền tảng nào. Ví dụ: bạn có con trỏ a
trong b
và ngược lại. Chúng được gán a.b = b
& b.a = a
. Và nếu cả hai đều là tham chiếu bền vững (strong) tới vùng nhớ. Nó sẽ tạo nên một vòng lặp vô tận chiếm giữ bộ nhớ. Khi đó, bộ nhớ sẽ không bị giải phóng. Người ta gọi là retain cycle.
Vấn đề này cũng dễ gặp phải với Delegate & Closure Callback. Ta xem thử nó được giải quyết như thế nào:
Delegate Pattern
class ImageDownloader { weak var delegate: ImageDownloaderDelegate? //... } class ImageViewer: ImageDownloaderDelegate { let downloader: ImageDownloader() init(url: URL) { downloader.delegate = self downloader.downloadImage(url: url) } func imageDownloader(_ downloader: ImageDownloader, didDownloadImage image: UIImage) { // view the downloaded image... } }
Trong đó:
- Với
delegate
của ImageDownloader sẽ là đối tượng nắm giữ tham chiếu chính. Và nó được khai báo vớiweak
, đó là một liên kết yếu. Và không làm tăngretain count
. - Mối quan hệ giữa
weak
&strong
thì được định nghĩa tại một chỗ. Không thể nào 1weak
lại trỏ tớiweak
khác được. - Bạn có thể bắt lỗi cú pháp này cho Delegate Callback bằng các tool như SwiftLint ….
Callback Pattern
class ImageDownloader { var didDownload: ((UIImage?) -> Void)? //... } class ImageViewer { let downloader: ImageDownloader init(url: URL) { downloader = ImageDownloader() downloader.downloadImage(url: url) downloader.didDownload = { [weak self] image in // view the image } } }
Trong đó:
- ImageViewer tham chiếu lại chính nó với Closure callback.
- Mối quan hệ
weak
&strong
phải được xác định chính xác trong mọi callback weak self
được sử dụng như là một cứu cánh trong trường hợp này.- Nó dễ gây ra việc
leak
bộ nhớ.
Trong trường hợp này thì Delegate Callback chiếm ưu thế nhiều hơn. Do nó chỉ cần định nghĩa weak
một lần duy nhất.
2.2. One to many relationships
Đời vẫn không như là mơ.
Không đơn giản một class/struct thì có một thể hiện duy nhất. Bạn có thể tạo ra và dùng nhiều đối tượng từ một class nào đó. Trong ví dụ của chúng ta ở trên, thì bạn có thể phải download nhiều ảnh với nhiều mục đích. Khi đó, bạn phải sử dụng nhiều đối tượng ImageDownloader. Và khi này sẽ ra sao?
Delegate Pattern
class ProfilePage: ImageDownloaderDelegate { let profilePhotoDownloader = ImageDownloader() let headerPhotoDownloader = ImageDownloader() init(profilePhotoUrl: URL, headerPhotoUrl: URL) { profilePhotoDownloader.delegate = self profilePhotoDownloader.downloadImage(url: profilePhotoUrl) headerPhotoDownloader.delegate = self headerPhotoDownloader.downloadImage(url: headerPhotoUrl) } func imageDownloader(_ downloader: ImageDownloader, didDownloadImage image: UIImage) { if downloader === profilePhotoDownloader { // show the profile photo... } else if downloader === headerPhotoDownloader { // show the profile photo... } } }
Bạn phải làm công việc đi kiểm tra lại các đối tượng của ImageDownloader, đối tượng nào trong trường hợp download ảnh nào. Và nếu có nhiều ảnh phải download như vậy, thì công việc này khá là tẻ nhạt. Và dễ mắc sai lầm, rất có thể nhầm lẫn các đối tượng với nhau.
Nhiều bạn thắc mắc khi hỏi mình là: “Tại sao phải thêm tham số chính là class trong function của Protocol của chính nó.”
Và điều ám ảnh thực sự đó là bạn sẽ phải thường xuyên sử dụng công việc này khi ViewController của bạn có 2 UITableView trở lên.
Closure Pattern
class ProfilePage { let profilePhotoDownloader = ImageDownloader() let headerPhotoDownloader = ImageDownloader() init(profilePhotoUrl: URL, headerPhotoUrl: URL) { profilePhotoDownloader.didDownload = { [weak self] image in // show the profile image } profilePhotoDownloader.downloadImage(url: profilePhotoUrl) headerPhotoDownloader.didDownload = { [weak self] image in // show the header image } headerPhotoDownloader.downloadImage(url: headerPhotoUrl) } }
Khá đơn giản và rõ ràng. Với mỗi Callback của Closure, thì nó hoàn toàn tách biệt với mỗi đối tượng. Bạn không thể nào nhầm lẫn tại đây được. Do đó, với vấn đề này thì Closure Callback dành ưu thế hơn.
2.3. Datasources
À, nó không thực sự là Delegate. Nhưng nó là người anh em với Delegate và cũng là một dạng được sử dụng nhiều của Protocol. Với Callback này bạn sẽ triệu hồi được dữ liệu từ đối tượng khác.
Datasources Pattern
protocol SerialImageUploaderDataSource: class { var numberOfImagesToUpload: Int { get } func image(atIndex index: Int) -> UIImage func caption(atIndex index: Int) -> String } class SerialImageUploader { weak var dataSource: SerialImageUploaderDataSource? init(dataSource: SerialImageUploaderDataSource) { self.dataSource = dataSource } func startUpload() { guard let dataSource = dataSource else { return } for index in 0..<dataSource.numberOfImagesToUpload { let image = dataSource.image(atIndex: index) let caption = dataSource.caption(atIndex: index) upload(image: image, caption: caption) } } func upload(image: UIImage, caption: String) { // Upload the image... } }
Trong đó:
- Tất cả các function trong protocol đều là bắt buộc. Và khi
datasource
tồn tại là có nghĩa các phương thức của nó phải được triển khai. - Nếu đưa data source vào trong
init
, thì công việc báo cho các class khác biết rằng nó sẽ cần dữ liệu. - Khi bạn thêm một function nữa thì việc biên dịch sẽ báo lỗi và bạn sẽ phải implement thêm nó vào các class mà bạn sử dụng.
Closure Pattern
class SerialImageUploader { var numberOfImagesToDownload: (() -> Int)? var imageAtIndex: ((Int) -> UIImage)? var captionAtIndex: ((Int) -> String)? func startUpload() { guard let numberOfImagesToDownload = numberOfImagesToDownload, let imageAtIndex = imageAtIndex, let captionAtIndex = captionAtIndex else { return } for index in 0..<numberOfImagesToDownload() { let image = imageAtIndex(index) let caption = captionAtIndex(index) upload(image: image, caption: caption) } } func upload(image: UIImage, caption: String) { // Upload the image... } }
Quả thật là vất vả. Bạn sẽ thấy các callback là các Optional. Và lần đầu tiên sử dụng, thì có thể tất cả chúng đều trả về là nil
. Khi đó bạn phải triệu hồi tới toán tử guard
cho tất cả bọn chúng. Và khi bạn dùng Non-Optional, bạn phải khởi tạo chúng tại hàm init
.
À, … mà thôi!
Trong trường hợp này, nếu bạn dùng Closure callback, thì hiệu quả chỉ khi sử dụng một lần và dưới dạng Optional. Nếu không hoặc cần phải sử dụng nhiều, thì Protocol có vẻ okay hơn.
2.4. Scalability
Tính mở rộng của class/struct mà bạn đã định nghĩa. Nó cũng là một trong các đặc tính quan trọng. Chắc bạn không muốn đập đi xây lại toàn bộ project chỉ vì thêm 1 hay 2 chức năng. Và ta xem qua như sau:
Delegate Pattern
protocol ImageDownloaderDelegate: class { func imageDownloader(_ downloader: ImageDownloader, didDownloadImage image: UIImage) func imageDownloaderDidFail(_ downloader: ImageDownloader) func imageDownloaderDidPause(_ downloader: ImageDownloader) func imageDownloaderDidResume(_ downloader: ImageDownloader) } extension ViewController: ImageDownloaderDelegate { func imageDownloader(_ downloader: ImageDownloader, didDownloadImage image: UIImage) { } func imageDownloaderDidFail(_ downloader: ImageDownloader) { } func imageDownloaderDidPause(_ downloader: ImageDownloader) { } func imageDownloaderDidResume(_ downloader: ImageDownloader) { } }
Trong đó:
- Bạn có thể nhóm tất cả chúng vào một
extension
. Nếu thêm bớt gì thì bạn có thể tuỳ ý sử dụng. - Rất rõ ràng và hiệu quả
Closure Pattern
class ImageDownloader { var didDownload: ((UIImage?) -> Void)? var didFail: (() -> ())? var didPause: (() -> ())? var didResume: (() -> ())? } class ViewController: UIViewController { let downloader = ImageDownloader() override func viewDidLoad() { super.viewDidLoad() downloader.didDownload = { //... } downloader.didFail = { //... } downloader.didPause = { //... } downloader.didResume = { //... } } }
Cảm nhận của bạn sẽ như thế nào. Bạn sẽ phải tìm tới tất cả những nơi mà bạn đặt các callback này. Và bạn sẽ cập nhật lại từng cái một. Hoặc bạn phải nghĩ ra một cách là tạo một function để nhóm tụi nó lại với nhau. Với cách này thì nó sẽ khó mà thống nhất được. Ví dụ: setupCallback
, setupData
, callInit
…
Trong trường hợp này thì Delegate Pattern sẽ chiếm ưu thế nhiều hơn.
2.5. Enforcing the contract
Đây là điều mà bạn sẽ mọng đợi nhiều từ Xcode. Khi triển khai từng loại thì việc tuân thủ các điều kiện & gán các đối tượng & implement các function … là điều bắt buộc. Xcode sẽ có tự động giúp bạn chỉ ra chỗ nào chưa được triển khai khi biên dịch code hay không.
Delegate Pattern
Bạn sẽ không bao giờ sót được việc implement các function của Delegate. Xcode vẫn là người bạn tốt nhất.
Closure Pattern
…
Hoàn toàn không có gì hết khi bạn sử dụng các Closure callback. À, hơi đắng!
3. Lựa chọn
Việc lựa chọn này mang tính chất chủ quan và có ý nghĩa tham khảo giúp cho bạn có lựa chọn tốt hơn. Quan trọng là bản thân bạn phải tự tin hơn. Chúng ta sẽ có một vài quy tắc như sau:
3.1. You have a single callback
Trong trường hợp callback đơn như thế này thì Closure là tốt nhất. Bạn có thể khắc phục được nhược điểm Optional của Closure khi đưa chúng vào hàm khởi tạo.
class ImageDownloader { var onDownload: (UIImage?) -> Void init(onDownload: @escaping (UIImage?) -> Void) { self.onDownload = onDownload } }
3.2. Your callbacks are more like notifications
Khi callback được sử dụng như là một notification
hay triggers
. Thì sự linh hoạt của Closure Callback bị kém hiệu quả đi. Bạn chỉ khởi tạo và dùng Callback đó tại một vài nơi. Hầu như tái sử dụng lại hoàn toàn không hiệu quả.
Với Delegate Callback, đây sẽ là chính xác những gì bạn đang cần. Với nó bạn có thể thông báo lại mọi trạng thái với một hoặc nhiều lần cho các đối tượng khác biết. Có thể nó implement các function hay là không. Delegate sẽ hiệu quả khi tái sử dụng lại.
3.3. You need to become the delegate to multiple instances
Ở đây, Closure Callback hoàn toàn chiếm ưu thế. Các callback chỉ tập trung với từng đối tượng hoặc từng mục đích sử dụng riêng biệt. Việc triển khai logic sẽ đơn giản và tránh nhầm lẫn.
Còn với Delegate Callback, bạn sẽ phải kiểm tra & phân biệt từng đối tượng delegate với từng mục địch khác nhau. Nó sẽ dễ nhầm lẫn khi số lượng đối tượng delegate & function trong Protocol tăng lên.
3.4. Your delegate is actually a datasource
Lựa chọn tối ưu lúc này chính là Protocol. Khi bạn implement các function thì Xcode sẽ giúp bạn thực thi contract. Giúp bạn tìm ra lỗi một cách nhanh chóng.
3.5. Your have many callbacks, and they might change in the future
Đây chính là tính mở rộng sau này. Không chỉ đơn giản chỉ là callback nhiều, mà ta cũng phải tính toán tới khả năng mình có thể handler nỗi sự phát sinh logic nữa hay không. Và khi bạn lạm dụng Closure callback quá đà, thì việc tìm các luồng dữ liệu & sự kiện rất là vất vả.
Tạm kết
Đây quả thật là điều khó xử khi đưa cho bạn lời khuyên là cái gì tốt nhất.
- Với Protocol,
- Nó vẫn có chỗ đứng riêng trong iOS mà rất khó để thay thế được.
- Việc định nghĩa các Protocol sẽ xác định rõ kiểu dữ liệu phù hợp.
- Nếu bạn có cập nhật các Protocol thì trình biên dịch sẽ giúp bạn xác định chỗ cần phải chỉnh sửa.
- Đơn giản hoá các tham chiếu
weak
/strong
khi khai báo một lần tại một chỗ
- Với Closure,
- Sử dụng được mọi lúc, mọi nơi.
- Đơn giản đi sự phức tạp của code, mối quan hệ nhiều đối tượng và dễ đọc code hơn.
- Được dùng nhiều trong các ngôn ngữ mới và mô hình mới trong iOS.
- Là một xu thế
Okay! Mình xin kết thúc bài viết này tại đây. Mọi lựa chọn vẫn tuỳ thuộc vào bạn mà thôi. Hi vọng bài viết sẽ giúp ích được cho bạn. Nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
Cảm ơn bạn đã đọc bài viết này!
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)