Linh Tạ

Notification Center vs Multicast Delegate

Notification Center là công cụ dễ sử dụng và dễ setup khi bạn muốn nhiều object cùng nhận sự kiện của một object chung. Tuy nhiên, đây cũng là điểm yếu cực lớn của phương pháp này bởi việc quá thoải mái gửi và nhận thông tin làm logic và architecture của app trở nên thiếu tổ chức, khiến quá trình debug khó khăn hơn rất nhiều.

Notification vs Delegate

1. Notification

Về bản chất, notification là một phương thức truyền sự kiện cho toàn ứng dụng. iOS và các framework như Core Data hay UIKit sử dụng nó để thông báo các event mang tính hệ thống hoặc các event mà bất kì object tại bất kì phần nào cũng có thể subscribe.

Các object post notification không quan tâm bất cứ thông tin gì về các object đã subscribe còn các object subscribe không quan tâm đến việc notification đó được gửi từ đâu, như thế nào. Hơn nữa, mọi object trong app đều có thể đăng kí nhận hoặc gửi notification.

2. Delegate

Delegate được dùng khi một object cần object còn lại gửi data hoặc chạy các function cụ thể tại những thời điểm nhất định.


tableView.delegate = self
tableView.dataSource = self

Ở ví dụ trên, table view dùng data từ delegatedataSource cho các tác vụ liên quan đến touch event và các tác vụ tạo cell như cellForRow, numberOfSection, numberOfRowsInSection. Ngược lại, self đăng kí làm table view delegate để có thể xử lý logic liên quan đến vòng đời của cell và tương tác từ người dùng.

Khi gán delegate, một mối quan hệ ngầm đã được thiết lập. self quan tâm rằng nó nhận delegate từ chính xác table view này chứ không phải bất kì table view nào khác. Còn table view yêu cầu self phải thuộc protocol UITableViewDelegate để làm delegateUITableViewDataSource để trở thành dataSource của nó.

Đây chính là điểm khác nhau lớn nhất giữa delegate và notification. Ở một khía cạnh nào đó, object đăng kí làm delegate và object nhận delegate đều “quan tâm” đến nửa kia.

3. Khi nào thì dùng delegate thay vì notification

Chỉ nên sử dụng Notification Center khi bạn thực sự cần khả năng truyền sự kiện cho toàn ứng dụng. Nếu thấy mình dùng Notification Center từ một object chung để bắn thông báo cho các object thuộc một type hoặc một protocol nhất định thì khả năng cao thứ bạn cần là delegate.

Một dấu hiệu khác của việc bạn nên dùng delegate là khi các object subscribe notification có thể truy cập object post notification, hoặc đơn giản là chúng ở cùng một luồng logic.

Dùng notification trong khi đáng lí ra phải dùng delegate khiến việc truyền data khó khăn hơn bởi Notification Center gửi parameter qua dictionary userInfo. Việc này không hề type safe và dễ xảy ra sai sót, dẫn đến việc bạn phải liên tục check type và xử lý optional khi lấy data từ userInfo.

Tệ hơn nữa, lạm dụng notification khiến luồng logic của app hỗn loạn và khó hiểu hơn rất nhiều bởi bất kì object, ở bất kì luồng nào, tại bất cứ thời điểm nào đều có thể gửi và nhận notification. Điều này cũng vô hình chung làm code trở nên khó debug và refactor.

Multicast Delegate

1. WeakObject

Delegate thường được dùng trong các mối quan hệ 1 - 1 nhưng không có nghĩa chúng ta không thể sử dụng nó cho quan hệ 1 - n. Chúng ta sẽ cần một mảng chứa delegate mà không gây ra retain cycle.


class WeakObject<T: AnyObject>: Hashable {
    private(set) weak var object: T?
    let identifier: ObjectIdentifier

    static func == (lhs: WeakObject<T>, rhs: WeakObject<T>) -> Bool {
        return lhs.identifier == rhs.identifier
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(identifier)
    }

    init(object: T) {
        self.object = object
        self.identifier = ObjectIdentifier(object)
    }
}

WeakObject là wrapper class dùng để lưu các delegate object dưới dạng weak. Mỗi khi cần thêm delegate, ta sẽ bọc nó trong WeakObject và lưu vào một mảng thuộc type này.

Nhằm tránh gán trùng delegate dẫn đến khả năng một delegate function bị gọi nhiều lần, mảng của WeakObject không thể là Array mà phải là Set. Đây cũng là lí do ta conform WeakObject với protocol Hashable. Sử dụng ObjectIdentifier, các WeakObject sẽ được phân biệt với nhau bằng địa chỉ vùng nhớ của object được bọc.

Phép so sánh object A === B trong swift thực chất chính là so sánh ObjectIdentifier(A) == ObjectIdentifier(B). Hai ObjectIdentifier == nhau nếu chúng đều trỏ đến cùng 1 vùng nhớ.

2. WeakDelegateCollection

Để đơn giản hoá việc quản lí và tương tác với mảng WeakObject ta sẽ tạo ra class WeakDelegateCollection cung cấp các chức năng cơ bản như thêm, xoá, và kích hoạt delegate function.


class WeakDelegateCollection<T> {
    typealias Canceller = () -> Void
    private var objects: Set<WeakObject<AnyObject>> = []

    @discardableResult
    func add(_ delegate: T) -> Canceller {
        //WeakObject chỉ hoạt động với reference type
        guard type(of: delegate as Any) is AnyClass else {
            fatalError("Class misused: WeakDelegateCollection should only be use with class")
        }

        //Chủ động cast delegate to be AnyObject bởi swift đang nhận diện delegate thuộc type T - vừa có thể là class vừa có thể là struct
        //Sau khi cast ta có thể dùng [weak delegate] để tránh retain cycle trong Canceller
        let delegate = delegate as AnyObject
        let object = WeakObject(object: delegate)
        objects.insert(object)

        //Sau khi gọi add có thể lưu lại Canceller để chủ động huỷ đăng ký
        return { [weak self, weak delegate] in
            guard let self = self, let delegate = delegate as? T else { return }
            self.remove(delegate)
        }
    }

    func remove(_ delegate: T) {
        guard type(of: delegate as Any) is AnyClass else { return }
        objects.remove(WeakObject(object: delegate as AnyObject))
    }

    func trigger(_ action: (T) -> Void) {
        for weakObject in objects {
            //Chạy closure nếu object trong WeakObject chưa deinit
            if let object = weakObject.object as? T {
                action(object)
            } else {
                //Xoá WeakObject có object đã được deinit
                objects.remove(weakObject)
            }
        }
    }
}

Ngoài việc bọc delegate trong WeakObject, function add(_ delegate:) trả ra một Canceller giúp ta có thể chủ động huỷ đăng ký mà không cần truy cập đến object bắn delegate.

Trong trường hợp không huỷ đăng ký, WeakDelegateCollection sẽ tự động xoá WeakObject nếu object bọc trong đó đã deinit. Điều này đảm bảo ta có thể gán delegate và hoàn toàn yên tâm về việc mọi object sẽ được giải phóng hợp lí mà không cần căn thời điểm remove.

Khi cần gọi các delegate function, ta truyền chúng vào trong closure của function trigger(_ action:). trigger chạy closure lần lượt với từng WeakObject với điều kiện object được bọc trong WeakObject đó vẫn tồn tại.

3. Sử dụng WeakDelegateCollection


protocol Executable: AnyObject {
    func doSomething(with item: Item)
    func doOtherThing()
}

class ControllerA: UIViewController, Executable { ... }
class ControllerB: UIViewController, Executable { ... }

class Service {
    let item = Item()
    let delegates = WeakDelegateCollection<Executable>()

    func showA() {
        let vcA = ControllerA()
        delegates.add(vcA)
        ...
    }

    func showB() {
        let vcB = ControllerB()
        delegates.add(vcB)
        ...
    }

    func receiveEvent() {
        delegates.trigger { [weak self] delegate in
            guard let self = self else { return }
            delegate.doSomething(with: self.item)
        }
    }

    func receiveOtherEvent() {
        delegates.trigger { delegate in
            delegate.doOtherThing()
        }
    }
}


Lưu ý

Trên mạng có một cách implement Multicast Delegate rất phổ biến khác với NSMapTable. Về cơ bản, ta thay thế WeakObject set với NSMapTable cung cấp bởi Apple. Tuy nhiên NSMapTable tồn tại một bug khá khó chịu khi nó cản trở việc deinit của delegate, khiến các delegate đấy retain lâu hơn dự kiến. Bug này có thể ảnh hưởng đến logic code bởi delegate function sẽ bị gọi với những object đúng ra đã được release.

Đọc thêm về bug này ở đây, đây, và ở đây. Ngoài ra mình có tạo một project mẫu biểu diễn vấn đề này trên github.

Tổng kết

Notification Center vẫn là lựa chọn tốt nhất nếu ta cần thông báo sự kiện cho toàn app. Multicast Delegate không thay thế hoàn toàn Notification Center nhưng khi được dùng đúng chỗ sẽ giúp các luồng logic trong codebase chặt chẽ, dễ mở rộng, dễ debug hơn rất nhiều. Đừng quên rằng mỗi công cụ hay design pattern đều có điểm mạnh và điểm yếu riêng. Sử dụng đúng công cụ cho đúng trường hợp luôn là quyết định cực kì quan trọng 😉.

Source code: Multicast Delegate