Linh Tạ

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 currentUseruserHandler 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!