Xử lý thay đổi trạng thái khi có nhiều object phụ thuộc
Khi data hoặc state chung thay đổi, việc đảm bảo các object phụ thuộc được đồng bộ và có cơ hội phản ứng với thay đổi đó là điều tối quan trọng. Đây cũng là một trong những lí do các thư viện reactive như RxSwift hay Combine được tạo ra. Tuy nhiên không phải lúc nào chúng ta cũng có thể sử dụng chúng trong dự án. Ở bài viết ngày hôm nay, cùng mình tìm hiểu một phương pháp đơn giản giúp giải quyết bài toán trên mà không cần đến thư viện của bên thứ 3.
Bài toán
Giả sử chúng ta có SessionManager
với nhiệm vụ lưu và quản lý data của session, trong đó có object currentUser
chứa các thông tin về người dùng đang login như tên tuổi, địa chỉ, email, hoặc avatar
struct User {
var name: String
var age: Int
var address: String
var email: String
var avatar: UIImage?
}
class SessionManager {
private(set) var currentUser: User
//...các thuộc tính, function khác...
}
Nhiều màn khác nhau phụ thuộc vào currentUser
để hiển thị UI như MainViewController
, SettingsViewController
, UserDetailsViewController
, … Vì các màn này luôn cùng tồn tại, ta muốn chúng được cập nhật UI và logic khi currentUser
thay đổi
class MainViewController {
let manager: SessionManager
//...
}
class SettingsViewController {
let manager: SessionManager
//...
}
class UserDetailsViewController {
let manager: SessionManager
//...
}
Phương pháp truyền thống
Hai phương án thường gặp trong trường hợp này là sử dụng Key-Value Observing
(KVO) với function observeValue(forKeyPath:of:change:context:)
hoặc bắn và nhận notification từ NotificationCenter
.
Cả 2 cách trên đều không tối ưu và có điểm yếu riêng. Với KVO, tại mỗi màn subscribe, chúng ta bắt buộc phải removeObserver
để tránh retain cycle. Hơn nữa, việc đăng ký nhận update tại nhiều màn khác nhau rất cồng kềnh và tốn công bởi cần nhiều boilerplate.
NotificationCenter
cũng gặp chung vấn đề với boilerplate. Không chỉ vậy, trong trường hợp chỉ một vài màn phụ thuộc vào currentUser
thì việc bất kì luồng nào trong app, dù không liên quan đến SessionManager
, cũng có thể nhận hoặc bắn!!? notification là bất hợp lí về mặt logic. Điểm yếu này tuy mang lại sự tiện lợi cho người code nhưng lại gây ảnh hưởng không nhỏ tới architecture của app và luồng đi của data.
Ưu tiên Dependency Injection thay vì sử dụng global data nằm ngoài object
Custom class Handler
Học tập RxSwift và Combine, ta sẽ tạo ra class Handler
có nhiệm vụ lưu closure xử lý của các object phụ thuộc và kích hoạt các closure này khi currentUser
thay đổi
class Handler<T> {
typealias Canceller = () -> Void
private var handlers: [ObjectIdentifier: (T) -> Void] = [:]
//Sử dụng lock khi truy cập `handlers`
//để đảm bảo class Handler an toàn khi sử dụng đa luồng
//NSRecursiveLock giúp tránh deadlock trong trường hợp
//closure của object cũng subscribe tới cùng handler
private var lock = NSRecursiveLock()
//Kích hoạt handler của các object subscribe
func trigger(_ action: T) {
lock.lock()
handlers.values.forEach { $0(action) }
lock.unlock()
}
//Mỗi object chỉ có thể đăng ký duy nhất 1 handler
//Trả ra Canceller thuộc type () -> Void
//để các subscribe object có thể huỷ handler chủ động
@discardableResult
func add(_ handler: AnyObject, closure: @escaping (T) -> Void) -> Canceller {
lock.lock()
//Sử dụng ObjectIdentifier để lưu id của object
//giúp tránh retain cycle
let id = ObjectIdentifier(handler)
//Tạo handler mới với closure được truyền vào
//handler có thể tự huỷ nếu object đã được deinit
handlers[id] = { [weak self, weak handler] newValue in
handler == nil ? self?.remove(id) : closure(newValue)
}
lock.unlock()
//Trả ra Canceller
return { [weak self] in self?.remove(id) }
}
private func remove(_ handler: ObjectIdentifier) {
lock.lock()
handlers.removeValue(forKey: handler)
lock.unlock()
}
}
//Function tiện lợi trong trường hợp T là Void
extension Handler where T == Void {
func trigger() {
trigger(())
}
}
Giờ đây chỉ những object thực sự quan tâm tới currentUser
mới có thể subscribe và phản ứng với thay đổi. Handler
không chỉ đơn giản hoá việc đồng bộ dữ liệu mà còn giúp tránh retain cycle và tự động giải phóng handler của các object đã deinit
. Việc subscribe cũng trở nên nhẹ nhàng hơn rất nhiều khi tất cả những gì ta cần làm là gọi function add(_:closure:)
.
Đối với SessionManager
, mỗi khi currentUser
update, nó chỉ cần gọi function trigger(_:)
, truyền vào giá trị mới nhất của currentUser
, và không cần quan tâm tới bất kì thứ gì khác
class SessionManager {
private(set) var currentUser: User {
didSet {
//Kích hoạt update mỗi khi có thay đổi
userHandler.trigger(currentUser)
}
}
private(set) var userHandler: Handler<User>
}
class MainViewController: UIViewController {
let manager: SessionManager
override func viewDidLoad() {
super.viewDidLoad()
manager.userHandler.add(self) { [weak self] newUser in
//...update UI hoặc xử lý logic...
}
}
}
Tổng kết
Handler
chỉ là một trong rất nhiều hướng tiếp cận với bài toán đồng bộ trạng thái và dữ liệu. Bạn có thể cải thiện Handler
bằng cách giữ data trong nó thay vì tạo ra 2 biến currentUser
và userHandler
như trên tuy nhiên đó không phải là mục đích chính của bài viết này. Mong rằng bài viết sẽ có ích với bạn và hẹn gặp lại!