Result type trong xử lý Networking

by LamNS
377 views

1. Giới Thiệu

Chào mọi người, mình là Lâm.

Hôm này mình xin viết 1 bài về Result type trong Swift và cách nó kết hợp với xử lý Networking để viết ra những đoạn code ngắn gọn, súc tích, dễ đọc, dễ maintain. Trước Swift 5 thì nó là 1 custom type phải tự viết, tuy nhiên sau đấy chắc mn thấy nó hay nên từ Swift 5 thì nó đã trở thành 1 type build-in của Swift luôn (https://developer.apple.com/documentation/swift/result) .

Có thể thấy ngôn ngữ Swift phát triển liên tục và nhiều trong số đấy là từ những ý tưởng đóng góp của cộng đồng developer

2. Ý tưởng

Khi xử lý những tác vụ trong lập trình, chúng ta luôn mong muốn là khi input 1 cái gì đấy vào, thì output của nó ra sẽ chỉ là 1 trong 2 case: success hoặc fail. Như vậy thì luồng xử lý của chúng ta sau đấy sẽ rất clear và gọn gàng.
Tuy nhiên success thì có nhiều kiểu giá trị trả về, fail thì cũng có nhiều kiểu lỗi bắn ra, vậy thì mình cần phải có 1 type để uniform được các trường hợp, quy về 1. Thì đấy chính là mục đích của Result type.

Ý tưởng của Result type rất đơn giản, nó là 1 generic Enum, gồm 2 generic là Success và Failure, và có 2 case: 1 cho success và 1 cho failure.
– Case success sẽ chứa value mà chúng ta expect.
– Case failure sẽ chứa error bắn ra.

enum Result < Success, Failure: Error > {
    case success(Success)
    case failure(Failure)
}

3. Áp dụng

Nói về xử lý Networking, ví dụ trong mô hình MVC, thường chúng ta sẽ tạo ra 1 func thế này ở Model:

func loadData(url: URL, completion: @escaping (Data?, Error?) -> Void) {
    let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
        if let error = error {
            completion(nil, error)
        }
         if let data = data {
             completion(data, nil)
        }
    }
    task.resume()

}


Rồi ở bên ViewController chúng ta sẽ hứng và xử lý tiếp như thế này:

model.loadData(url: url) { [weak self] (data, error) in
    guard let self = self else { return }
    if let error = error {
        // handle error...
    }
     if let data = data {
        // handle response...
    }
}

Có 2 cái ko hay ở đây:
– Cái ko hay thứ 1 là với completion: (Data?, Error?) chúng ta sẽ có tổng cộng 4 case xảy ra:
+ (data, error)
+ (data, nil)
+ (nil, error)
+ (nil, nil)
Case đầu tiên (data, error) và case cuối cùng (nil, nil) là 2 case mà sẽ không bao giờ mong muốn. Vì output ko thể nào vừa có data lại vừa có error, hay output ko có gì cả, như vậy thì sẽ rất confuse.

– Cái ko hay thứ 2 là ở đầu bên kia (ViewController) khi nhận response từ Model sẽ phải xử lý 2 parameter với type là optional (data và error), rồi lại if/else 1 hồi để handle đủ case.

Bây giờ nếu chúng ta sử dụng Result type vào thì nó sẽ như thế này:

func loadData(url: URL, handler: @escaping (Result<Data, Error>) -> Void) {

        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            if let error = error {
                completion(.failure(error))
            }
            if let data = data {
                completion(.success(data))
            }
        }
    }

và đầu bên kia xử lý sẽ như thế này:

        model.loadData(url: url) { [weak self] result in
            guard let self = self else { return }
            switch result {
            case .failure(let error):
                // handle error...
            case .success(let data):
                // handle success...
            }
        }

sẽ chỉ phải xử lý 1 parameter là result: Result, và Result chỉ có 2 case là success/fail, đúng với những gì cta mong muốn, code nhìn clear hơn nhiều, thay đống if/else bằng switch/case cũng làm code dễ đọc hơn.

4. Mở rộng

Về cơ bản là như vậy, mở rộng hơn thì chúng ta có thể define chi tiết hơn về lỗi.
Nhìn lại declaration:

enum Result < Success, Failure: Error > {
    case success(Success)
    case failure(Failure)
}

chúng ta thấy Failure đang đơn giản adopt vào protocol Error. Error thì rất common, nó khá là chung chung, gọi là lỗi gì vứt vào đây cũng được. Bây giờ muốn specific hơn về lỗi, cta có thể tạo 1 custom type, vì Result của cta nhận vào generic là 1 Error nên custom type của cta chỉ cần adopt vào protocol Error là được:

enum VideoError: Error {
    case networkFailure(Error)
    case exceedSize
    case invalidFormat
}

khi gọi API:

    func loadData(url: URL, completion: @escaping (Result<Data, VideoError>) -> Void) {

        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in

            if let error = error {
                completion(.failure(.networkFailure(error)))
                return
            }

            if exeedSize {
                completion(.failure(.exceedSize))
                return
            }

            if invalidFormat {
                completion(.failure(.invalidFormat))
                return
            }

            if let data = data {
                completion(.success(data))
            }
        }
    }

và đầu bên kia xử lý:

       model.loadData(url: url) { [weak self] result in
            guard let self = self else { return }
            switch result {
            case .success(let data):
                // handle success...
            case .failure(.networkFailure(let error)):
                // handle networking error...
            case .failure(.exceedSize):
                // handle exceedSize...
            case .failure(.invalidFormat):
                // handle invalid format...
            }
        }

có thể thấy luồng đi rất clear.

Bên trên là Result mình tự viết, đối với built-in của Swift 5 thì type còn có 1 số những methods:
get: để lấy ra success value
map/flatMap: để transform success value thành 1 dạng khác
mapError/flatMapError: để transform failure error thành 1 dạng khác
==, != : để so sánh xem 2 result có giống nhau hay ko
Mn có thể đọc chi tiết trong docs:
https://developer.apple.com/documentation/swift/result

Nói chung nó sẽ hỗ trợ để mình xử lý thêm cho data/error trả về (hình dung mình có thể viết 1 cái extension để decode success value thành object luôn chẳng hạn).

OK, hy vọng mn cảm thấy hữu ích.
Thanks for reading.

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.