12/08/2018, 14:07

Error handling in Asynchronous Programming

1. Đặt vấn đề Trở lại với chủ đề chúng ta đã từng bàn trong topic này Post, ở topic trước mình đã trình bày cách sử dụng closure để xử lý data từ 2 api bất đồng bộ trả về nhưng còn 1 vấn đề đó nữa là các lỗi(error) thì chưa đc xử lý, chẳng hạn nếu api thứ nhất bị lỗi do mất kết nối đến server ...

1*SveX5uh786Eg-Iw-IQVENA.jpeg

1. Đặt vấn đề

Trở lại với chủ đề chúng ta đã từng bàn trong topic này Post, ở topic trước mình đã trình bày cách sử dụng closure để xử lý data từ 2 api bất đồng bộ trả về nhưng còn 1 vấn đề đó nữa là các lỗi(error) thì chưa đc xử lý, chẳng hạn nếu api thứ nhất bị lỗi do mất kết nối đến server thì nó chỉ in được màn hình console chứ chưa handle error đó lại để mình xử lý. Trong bài viết này mình sẽ thực hiện handle các error nếu có trong từng api và xử lý các error đó.

2. Giới thiệu về Result

Trở lại với phiên bản 1.0 của swift khi mà throw chưa có, để xử lý lỗi người ta thường xử dụng 1 pattern gọi là Result.

enum Result<T> {
  case Success(T)
  case Failure(ErrorType)
}

Pattern này rất đơn giản, chỉ có 2 khái niệm:

  • Success: trả về 1 kiểu giá trị
  • Failure: trả về 1 kiểu lỗi sau đó rất đơn giản chúng ta chỉ việc gọi hàm complete với 2 kiểu: Result.Success hoặc Result.Failure

3. Giới thiệu về Monad

Mọi người có thể đọc thêm về Monad Ở Đây

Một ý tưởng rất hay đó là chúng ta tạo 1 lớp Async có các thuộc tính :

  • result: đại diện cho trạng thái các xử lý callback.
  • handlers: chứa các callback mà ta muốn xử lý.
  • function then trả về 1 Monad.

Đầu tiên ta viết lại Result như sau:

enum Result {
    case Waiting
    case Success(Any)
    case Failure(ErrorType)
}

Result sẽ có 3 trạng thái:

  • Khi mới khởi tạo nó sẽ có trạng thái Waiting: chờ được xử lý
  • Khi 1 async được thực hiện xong result sẽ nhận 1 trong 2 trạng thái: Success kèm giá trị hoặc failure kèm với error.

Tiếp theo ta tạo class Async với 1 kiểu giá trị generic T

class Async<T> {
    var result: Result = .Waiting
    var handlers:[(() -> Void)] = []

    var waiting:Bool {
        switch result {
        case .Failure, .Success: return false
        case .Waiting: return true;
        }
    }
}

Hàm khởi tạo cho 2 trường hợp success và failure

    init(value: T) {
        self.result = .Success(value)
    }

    init(error: ErrorType) {
        self.result = .Failure(error)
    }

Hàm khởi tạo cho chung cho cả 2 trường hợp:

init(_ f:(sucess:T -> Void, failure:ErrorType -> Void) -> Void) {
        func recurse() {
            for handler in handlers {
                handler()
            }

            handlers.removeAll(keepCapacity: false)
        }

        func failure(err: ErrorType) {
            if self.waiting {
                self.result = .Failure(err);
                recurse();
            }
        }
        func success(obj: T) {
            if self.waiting {
                self.result = .Success(obj);
                recurse()
            }
        }

        f(sucess: success, failure: failure)
    }

Ta tạo 1 hàm dispatch_asyncTask chạy trên main thread xử lý các tác vụ liên quan đến UI:

func dispatch_asyncTask<T>(f:(success: T -> Void, failure: ErrorType ->Void) -> ()) -> Async<T> {
        return Async<T> { (success, failure) in
            dispatch_async(dispatch_get_main_queue()) {
                f(success: success, failure: failure)
            }
        }
    }

Ta tạo hàm then đầu vào có thể là closure f:T -> U hoặc f:T -> Async<U> trả về Async<U>

    func then<U>(f:T -> U) -> Async<U> {
        switch result {
        case .Failure(let error):
            return Async<U>(error: error);
        case .Success(let value):
            return dispatch_asyncTask(){ d in
                d.success(f(value as! T))
            }
        case .Waiting:
            return Async<U> { (success, failure) in
                self.handlers.append {
                    switch self.result {
                    case .Failure(let error):
                        failure(error)
                    case .Success(let value):
                        dispatch_async(dispatch_get_main_queue()) {
                            success(f(value as! T))
                        }
                    case .Waiting:
                        break
                    }
                }
            }
        }
    }

    func then<U>(f:T -> Async<U>) -> Async<U> {
        func fill(value:T, success: U -> (), failure: ErrorType -> ()) {
            let promise = f(value)
            switch promise.result {
            case .Failure(let error):
                failure(error)
            case .Success(let value):
                success(value as! U)
            case .Waiting:
                promise.handlers.append{
                    switch promise.result {
                    case .Failure(let error):
                        failure(error)
                    case .Success(let value):
                        success(value as! U)
                    case .Waiting:
                        break
                    }
                }
            }
        }

        switch result {
        case .Failure(let error):
            return Async<U>(error: error);
        case .Success(let value):
            return dispatch_asyncTask(){
                fill(value as! T, success: $0, failure: $1)
            }
        case .Waiting:
            return Async<U> { (success, failure) in
                self.handlers.append{
                    switch self.result {
                    case .Waiting:
                        break
                    case .Success(let value):
                        dispatch_async(dispatch_get_main_queue()) {
                            fill(value as! T, success: success, failure: failure)
                        }
                    case .Failure(let error):
                        failure(error)
                    }
                }
            }
        }
    }

4. Xử lý lỗi

Để handle error trả về từ các callback ta có hàm error:

    func error(f:ErrorType -> T) -> Async<T> {
        switch result {
        case .Success(let value):
            return Async(value:value as! T)
        case .Failure(let error):
            return dispatch_asyncTask(){ (success, _) -> Void in
                success(f(error))
            }
        case .Waiting:
            return Async<T>{ (success, _) in
                self.handlers.append {
                    switch self.result {
                    case .Success(let value):
                        success(value as! T)
                    case .Failure(let error):
                        dispatch_async(dispatch_get_main_queue()){
                            success(f(error))
                        }
                    case .Waiting:
                        break
                    }
                }
            }
        }
    }

Tạo 1 enum định nghĩa các lỗi có thể xảy ra trong ứng dụng:

enum RequestErrors: ErrorType {
    case CouldNotParseJSON
    case NoInternetConnnection
    case UnknowError
}

Từ đó ta có thể viết hàm getSongByArtist như sau:

func getSongByArtist(artistName: String) -> Async<[SongModel]> {
        return Async { success, failure in
            let url = "https://itunes.apple.com/search?media=music&entity=song&term=(artistName)"

            Alamofire.request(.GET, url)
                .responseJSON{ response in
                    switch response.result {
                    case .Success(let value):
                        let json = JSON(value)

                        if let songDataArray = json["results"].array {
                            var songs = [SongModel]()

                            for songData: JSON in songDataArray {
                                if let
                                    trackId = songData["trackId"].int,
                                    trackName = songData["trackName"].string,
                                    artistId = songData["artistId"].int,
                                    artistName = songData["artistName"].string {
                                    let song = SongModel(trackId: trackId, trackName: trackName, artistId: artistId, artistName: artistName)
                                    songs.append(song)
                                }
                            }

                            success(songs)
                        } else {
                            failure(RequestErrors.CouldNotParseJSON)
                        }
                    case .Failure(let error):
                        if let err = error as? NSURLError where err == .NotConnectedToInternet {
                            failure(RequestErrors.NoInternetConnnection)
                        } else {
                            failure(RequestErrors.UnknowError)
                        }
                    }
            }
        }
    }
}

Qua ví dụ trên ta có thể thấy việc handle các error trong ứng dụng đã đc thực hiện.

Ở ViewController ta viết lại hàm getDataSong như sau:

func getDataSong() {
        self.updateTextStatus("Downloading Taylor Swift ... ", progress: 0.0).then{
            self.songDataProvider.getTaylorSwiftSongs()
            }.then { songs in
                self.completeLoadTaylorSwiftSong(songs)
            }.then { _ in
                self.updateTextStatus("Downloading Westlife ...", progress: 0.5)
            }.then {
                self.songDataProvider.getWestlifeSongs()
            }.then { songs in
                self.completeLoadWestlifeSong(songs)
            }.then { _ in
                self.updateTextStatus("Done", progress: 0.0)
            }.error { error in
                self.handleError(error)
        }
    }

Hàm handleError

func handleError(error: ErrorType) -> Async<Void> {
        return Async { success, failure in
            if let error = error as? RequestErrors {
                switch error {
                case .CouldNotParseJSON:
                    Alert.alertError("Could not parse josn data!", inViewController: self, withDismissAction: {})
                case .NoInternetConnnection:
                    Alert.alertError("You are not connect to internet!", inViewController: self, withDismissAction: {})
                case .UnknowError:
                    Alert.alertError("Unknown error!", inViewController: self, withDismissAction: {})
                }
            }
            success()
        }
    }

Tuỳ vào từng error chúng ta có thể đưa ra các xử lý khác nhau.

5. Chạy thử ứng dụng

5.1 Ngắt kết nối device với internet

Simulator Screen Shot Oct 29, 2016, 11.49.39 PM.png

Lúc này ta thấy ngay lỗi trả về cho chúng ta biết device không được kết nối với internet.

5.2 Sửa param ở hàm getSongByArtist để việc parse data bị lỗi

Simulator Screen Shot Oct 29, 2016, 11.51.48 PM.png

6. Kết luận

Việc handle error đã làm code của chúng ta rõ ràng, ngắn, dễ bảo trì hơn, qua đó giúp chúng ta cải thiện hiệu quả công việc giảm tình trạng lỗi phát sinh khi xây dựng ứng dụng.

7. Demo

https://github.com/pqhuy87it/MonthlyReport/tree/master/ErrorHandlingInAsynchronousProgramming

8. Tham khảo

http://alisoftware.github.io/swift/async/error/2016/02/06/async-errors/ https://medium.com/@zhxnlai/async-programming-in-swift-with-asynctask-95a708c1c3c0#.x227098yx https://gist.github.com/oleganza/7342ed829bddd86f740a

0