Closure callback có thể thay thế được Delegate không?

Chắc hẳn ai code IOS cũng không hề xa lạ với khái niệm delegate - đó là một design pattern tuyệt vời giúp chúng ta có thể uỷ thác cho một đối tượng khác có thể thực hiện một hành động thay nó. Với Swift chúng ta cũng có một pattern tuyệt vời không kém để thực hiện nhiệm vụ tương tự như vậy đó là Closure. Vậy với Swift thì Closure có thay thế được Delegate không.

Hãy cùng xét một ví dụ, chúng ta có một class ImageDownloader muốn uỷ thác cho class ImageViewer thực hiện nhiện vụ hiển thị ảnh khi đã được download xong.

  • Với cách làm bằng delegate:
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)
    }
}
  • Còn với cách làm bằng closure:
class ImageDownloader {
    var didDownload: ((UIImage?) -> Void)?
    func downloadImage(url: URL) {
        // download the image asynchronously then...
        didDownload?(theImage)
    }
}

Breaking the retain cycle

Khi thực hiện việc uỷ thác cho một class khác tức là class khác sẽ giữ tham chiếu của class uỷ thác đó, và việc 2 đối tượng giữ tham chiếu của nhau sẽ dẫn đến retain cycle. Để tránh việc bị retain cycle giữa 2 class uỷ thác cho nhau chúng ta sẽ để một bên sẽ giữ weak reference của bên còn lại. Việc thực hiện đối với từng pattern cụ thể là:

  • Với 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...
    }
}
  • Đối với closure 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
        }
    }
}

Vậy là tuy khá cũ, nhưng cách làm của Delegate pattern là rõ ràng hơn hẳn, nên Delegate > Closure.

One to many relationships

Với trường hợp nếu chúng ta cần nhiều ImageDownloader thì sao? Nếu chúng ta dùng Delegate như thông thường thì chỉ có một function được implement, vậy để phân biệt là hành động của delegate nào bắn tới thì sẽ cần tạo ra thêm instance để lưu trữ và phân biệt chúng.

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...
        }
    }
}

Nếu với Closure việc thực hiện sẽ rõ ràng hơn rất nhiều, vì khi thực hiện chúng ta đã có callback luông đó và hoàn toàn riêng biệt. Với trường hợp này, Closure callback sẽ tốt hơn, nên tỉ số sẽ là 1-1 giữa closure và delegate.

Scalability

Những trường hợp ở trên chúng ta đã lấy ví dụ khi chỉ có một phương thức, vậy nếu chúng ta có 10 phương thức thì sao? Với Delegate thì hơn hẳn khi chúng rất rõ ràng và gói gọn trong một Extension Còn với Closure sẽ hơi khó khăn một chút hoặc nhiều chút khi chúng ta phải thực thi callbacks khi set up chúng, điều này sẽ khiến file phình to hơn rất nhiều. Delegate pattern phát huy hiệu quả hơn hẳn trong trường hợp này, 2-1 cho Delegate.

Enforcing the contract

Với mục đích là uỷ thác cho một đối tượng khác thực hiện hành động cho mình, nên Delegate sẽ yêu cầu tất cả các function sẽ phải được thực hiện, và khi thực hiện thiếu thì compiler sẽ báo lỗi luôn trong quá trình runtime. Điều này sẽ khiến chúng ta tránh được những lỗi không đáng có, và cũng sẽ dễ dàng cho người khác đọc vào hiểu được rõ ràng hơn ý định của người viết.

Còn đối với Closure callback, chúng ta sẽ không nhận được cảnh báo nào nếu như thực hiện thiếu một vài phương thức nào đó. Mong là điều này không quan trọng :v

Vậy Delegate và Closure cái nào tốt hơn

Dựa vào những phân tích trên thì có thể thấy Delegate hơn Closure ở nhiều mặt, nhưng không thể nói Delegate là tốt hơn vì Closure còn có rất nhiều tác dụng khác. Còn trong việc thay thế Closure cho Delegate thì là hoàn toàn được, nhưng có vẻ sẽ ít được rõ ràng bằng Delegate. Vì vậy nếu bạn có ý định dùng Closure thay thế cho delegate thì hãy cân nhắc vào từng trường hợp. Delegate hay Closure tốt hơn là tuỳ vào từng trường hợp và cách ta sử dụng chúng. Mong bài viết đã giúp ích cho các bạn trong việc nhìn nhận giữa Closure callback và Delegate (bow)

Reference:

https://itnext.io/delegates-vs-closure-callbacks-f36f9029217d