0

Mô hình hoá "State" trong swift

Một trong những bước khó nhất khi xây dựng các ứng dụng và thiết kễ hệ thống đó là việc quyết định mô hình hoá và xử lý các state như thế nào . Việc quản lý đoạn code liên quan đến state rất hay xảy ra lỗi , khi 1 phần ứng dụng của chúng ta có thể kết thúc với state mà ta không mong muốn.

Ở bài viết này, tôi sẽ chỉ cho bạn một số kỹ thuật giúp bạn dễ dàng viết code hơn và ít lỗi hơn khi phải đối mặt với việc mô hình hoá các state .

Một Nguồn Chuẩn Duy Nhất

Một nguyên tắc cốt lõi tốt nhất để nhớ khi mô hình hoá các state khác nhau là cố gắng bám lấy một “nguồn chuẩn duy nhất” càng chặt càng tốt . Một cách để làm điều này là bạn đừng bao giờ kiểm tra nhiều điều kiện để xác định là bạn đang ở state nào . Ví dụ :

Giả sử chúng ta đang xây dựng một game , ở đó kẻ thù có 1 sức khoẻ nhất định và 1 lá cờ để xác định xem nó có còn sống hay không , Chúng ta có thể mô hình hoá bằng việc sử dụng hai thuộc tính trên một lớp Enemy, như sau: :

class Enemy {
    var health = 10
    var isInPlay = false
}

Tuy nhiên nếu chúng ta rơi vào tình huống khi sức khoẻ của kẻ thù còn lại là 0 , kẻ thù phải biến mất khỏi trò chơi , vì vậy 1 nơi nào đó trong đoạn code của chúng ta có 1 đoạn xử lý logic :

func enemyDidTakeDamage() {
    if enemy.health <= 0 {
        enemy.isInPlay = false
    }
}

Vấn đề xảy ra khi bây giờ tôi muốn thêm 1 đoạn code mới nhưng quên không check đoạn code như bên trên . Ví dụ ,tôi cung cấp cho người chơi một cuộc tấn công đặc biệt, tiêu diệt toàn bộ kẻ thù :

func performSpecialAttack() {
    for enemy in allEnemies {
        enemy.health = 0
    }
}

Như bạn thấy đấy , tôi mới chỉ cập nhật sức khoẻ , nhưng quên chưa cập nhật isInPlay . Điều này có thể sẽ gây ra lỗi và tình huống kết thúc của tôi rơi vào trạng thái không xác định .

Ở tình huống trên ta có thể khắc phục bằng cách multi check như sau :

if enemy.isInPlay && enemy.health > 0 {
    // Enemy is *really* in play
} else {
    // Enemy is *really* defeated
}

Mặc dù đoạn code trên hoạt động bình thường , nhưng nó sẽ gây khó khăc trong việc đọc code logic và dễ dàng bị phá vỡ bất cứ khi nào nếu như tôi thêm một số điều kiện phức tạp hơn .

Một cách để giải quyết vấn đề này và để đảm bảo rằng chúng ta có một nguồn chuẩn duy nhất là tự động cập nhật thuộc tính isInPlay bên trong lớp Enemy, sử dụng một didSet trên thuộc tính health:

class Enemy {
    var health = 10 {
        didSet { putOutOfPlayIfNeeded() }
    }
    private(set) var isInPlay = true

    private func putOutOfPlayIfNeeded() {
        guard health <= 0 else {
            return
        }

        isInPlay = false
        remove()
    }
}

Với cách trên , chúng ta chỉ cần quan tâm đến sức khoẻ của kẻ thù , còn thuộc tính isInPlay sẽ luôn được cập nhật trực tiếp qua thuộc tính health 👍

Tạo ra các trạng thái riêng biệt

Ví dụ Enemy trên khá đơn giản, vì vậy chúng ta hãy nhìn vào một ví dụ khác khi chúng ta phải đối mặt với các state phức tạp hơn mà mỗi cái đều có các giá trị liên quan mà chúng ta cần phải giải quyết và đáp ứng phù hợp.

Giả sử chúng ta đang xây dựng trình phát video, điều này sẽ cho phép chúng ta tải xuống và xem một video từ một URL nhất định. Để mô hình hoá một video, chúng ta có thể sử dụng một cấu trúc như sau :

struct Video {
    let url: URL
    var downloadTask: Task?
    var file: File?
    var isPlaying = false
    var progress: Double = 0
}

Vấn đề với cách trên là chúng ta kết thúc với nhiều optionals, và chúng ta không thể đảm bảo được trạng thái mà một đoạn video có thể có chỉ bằng cách đọc code mô hình của chúng ta .Chúng ta thường kết thúc với việc phải viết 1 đoạn xử lý code phức tạp :

if let downloadTask = video.downloadTask {
    // Handle download
} else if let file = video.file {
    // Perform playback
} else {
    // Uhm... what to do here? 🤔
}

Cách tôi thường giải quyết vấn đề này là sử dụng một enum để xác định rõ ràng, trạng thái riêng biệt , như sau :

struct Video {
    enum State {
        case willDownload(from: URL)
        case downloading(task: Task)
        case playing(file: File, progress: Double)
        case paused(file: File, progress: Double)
    }

    var state: State
}

Như bạn thấy bên trên , tôi đã loại bỏ đi hết các optional và tất cả các giá trị state cụ thể được kết hợp vào state mà chúng sẽ được sử dụng . Chúng ta có thể loại bỏ một số trùng lặp bằng việc giới thiệu một cấp độ state khác cho việc phát lại thông tin

extension Video {
    struct PlaybackState {
        let file: File
        var progress: Double
    }
}

Sau đó, chúng ta có thể sử dụng cả trong trường hợp play và pause:

case playing(PlaybackState)
case paused(PlaybackState)

Tuy nhiên, nếu bạn bắt đầu mô hình hóa trạng thái của bạn như trên, nhưng bắt buộc vẫn phải xử lý đoạn code (sử dụng câu lệnh if/else), mọi thứ sẽ trở nên khá xấu. Vì tất cả các thông tin chúng ta cần là "hidden" bên trong các trường hợp khác nhau, chúng ta sẽ cần phải thực hiện nhiều câu lệnh switch.

Những gì chúng ta cần phải kết hợp enum state là mã xử lý state . Ví dụ: hãy xem cách tôi viết mã để cập nhật một nút hành động trong video player view controller:

class VideoPlayerViewController: UIViewController {
    var video: Video {
        // Every time the video changes, we re-render
        didSet { render() }
    }

    fileprivate lazy var actionButton = UIButton()

    private func render() {
        renderActionButton()
    }

    private func renderActionButton() {
        let actionButtonImage = resolveActionButtonImage()
        actionButton.setImage(actionButtonImage, for: .normal)
    }

    private func resolveActionButtonImage() -> UIImage {
        // The image for the action button is declaratively resolved
        // directly from the video state
        switch video.state {
            // We can easily discard associated values that we don't need
            // by simply omitting them
            case .willDownload:
                return .wait
            case .downloading:
                return .cancel
            case .playing:
                return .pause
            case .paused:  
                return .play
        } 
    }
}

Bây giờ mỗi khi trạng thái video của chúng ta thay đổi, UI của chúng ta sẽ tự động cập nhật. Chúng ta có một nguồn chuẩn duy nhất, và không có trạng thái không xác định 🎉 Sau đó chúng ta có thể mở rộng phương thức render để tự động cập nhật UI khi state thay đổi:

private func render() {
    renderActionButton()
    renderVideoSurface()
    renderNavigationBarButtonItems()
    ...
}

Trên đây là 1 số kỹ thuật mà theo cá nhân mình sẽ giúp ích khá nhiều cho những bạn đang lập trình IOS .

Hẹn gặp lại các bạn trong bài viết tới . Thank you for watching !! <3<3<3


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí