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. Đặ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
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
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