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ừ delegate
và dataSource
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 delegate
và UITableViewDataSource
để 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ánhObjectIdentifier(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