+1

SỬ DỤNG CLOSURE, PROTOCOL LÀM CODE GỌN GÀNG DỄ HIỂU HƠN.

Bài toán đặt ra

  • Ta có 1 ứng dụng lấy dữ liệu song từ API theo các artist.
  • Việc lấy dữ liệu thực hiện tuần tự theo các bước: lấy song artist 1 -> update UI -> lấy song artist 2 -> update UI.
  • Ta có đoạn code như sau:
func getDataSong() {
        statusLabel.text = "Download Taylor Swift Song ..."

        AppServices.getSongByArtist("TaylorSwift", failureHandler: { (reason, errorMessage) in
                print(errorMessage)
            }, completion: {[weak self] songs in
                if let songs = songs {
                    self?.taylorSwiftSongs.removeAll()
                    self?.taylorSwiftSongs.appendContentsOf(songs)

                    dispatch_async(dispatch_get_main_queue(), {
                        self?.statusLabel.text = "Download Westlife Song ..."
                        self?.statusProgressView.progress = 0.5
                        self?.tableView.reloadData()

                        AppServices.getSongByArtist("Westlife", failureHandler: { (reason, errorMessage) in
                                print(errorMessage)
                            }, completion: { songs in
                                if let songs = songs {
                                    self?.westlifeSongs.removeAll()
                                    self?.westlifeSongs.appendContentsOf(songs)

                                    dispatch_async(dispatch_get_main_queue(), {
                                        self?.statusLabel.text = "Done!"
                                        self?.statusProgressView.progress = 0.0
                                        self?.tableView.reloadData()
                                    })
                                }
                        })
                    })

                }
        })
    }

Sử dụng closure tối ưu lần 1

Ta định nghĩa 1 closure Action:

typealias Action = () -> Void

Từ đó ta tách phần get data song ra làm 2 hàm

func getTaylorSwiftSong(completion: Action) {
        AppServices.getSongByArtist("TaylorSwift", failureHandler: { (reason, errorMessage) in
                print(errorMessage)
            }, completion: {[weak self] songs in
                if let songs = songs {
                    self?.taylorSwiftSongs.removeAll()
                    self?.taylorSwiftSongs.appendContentsOf(songs)
                    completion()
                }
        })
    }

func getWestlifeSong(complete: Action) {
        AppServices.getSongByArtist("Westlife", failureHandler: { (reason, errorMessage) in
            print(errorMessage)
            }, completion: {[weak self] songs in
                if let songs = songs {
                    self?.westlifeSongs.removeAll()
                    self?.westlifeSongs.appendContentsOf(songs)
                    complete()
                }
            })
    }

Ta cũng tách phần update UI ra thành 1 hàm riêng

func updateStatus(text: String, progress: Float = 0.0, reload: Bool = true) {
        self.statusLabel.text = text
        self.statusProgressView.progress = progress

        if reload {
            self.tableView.reloadData()
        }
    }

Giờ đây hàm getDataSong của chúng ta đã trở nên gọn hơn nhiều so với trước:

func getDataSong() {
        updateStatus("Download Taylor Swift Song ...")
        getTaylorSwiftSong {
            dispatch_async(dispatch_get_main_queue(), {
                self.updateStatus("Download Westlife Song ...")
                self.getWestlifeSong {
                    dispatch_async(dispatch_get_main_queue(), {
                        self.updateStatus("Done!")
                    })
                }
            })
        }
    }

Giả sử chúng ta muốn get song từ nhiều artist nữa thì sao, sẽ có rất nhiều hàm lồng vào nhau, code nhìn sẽ rất rối.

challenge_considered.png

Sử dụng closure tối ưu lần 2

Ta định nghĩa thêm 1 closure AsyncTask

typealias AsyncTask = (Action) -> Void

Ta tạo thêm 3 function:

func |>(lhs: AsyncTask, rhs: Action) -> AsyncTask {
    return { (action) -> Void in
        lhs { rhs() ; action() }
    }
}

func |>(lhs: AsyncTask, rhs: Action) -> Action {
    return {
        lhs { rhs() }
    }
}

func |>(lhs: AsyncTask, rhs: AsyncTask) -> AsyncTask {
    return { (action) -> Void in
        lhs { rhs(action) }
    }
}

Chuyển hàm updateStatus từ (String, Float, Bool) -> Void thành (String, Float, Bool) -> Action

func updateStatus(text: String, progress: Float = 0.0, reload: Bool = true) -> Action{
        return {
            self.statusLabel.text = text
            self.statusProgressView.progress = progress

            if reload {
                self.tableView.reloadData()
            }
        }
    }

Sử dụng closure chuyển hàm dispatch_async(dispatch_get_main_queue() thành 1 property

let switchToMainThread: AsyncTask = { (action) -> Void in
        dispatch_async(dispatch_get_main_queue(), action)
    }

Hàm getDataSong được viết lại như sau:

func getDataSong() {
        let syncTaylorSwiftSong: AsyncTask = switchToMainThread |>
                                             updateStatus("Download TaylorSwift Song ...") |>
                                             getTaylorSwiftSong

        let syncWestlifeSong: AsyncTask = switchToMainThread |>
                                          updateStatus("Download Westlife Song...") |>
                                          getWestlifeSong

        let endSync: Action = switchToMainThread |>
                              updateStatus("Done!")

        let task: Action = syncTaylorSwiftSong |>
                           syncWestlifeSong |>
                           endSync
        task()
    }

Từ h nếu muốn lấy thêm song từ 1 artist nào khác ta chỉ việc thêm 1 asynTask khác rồi chèn vào trước endSync, nhìn rất gọn phải không

Có 1 vấn đền khác nữa nảy sinh, mỗi lần thêm 1 artist ta phải tạo thêm 1 hàm get song từ artist đó, công thêm ở phần tableView lại phải if else theo section từng artist.

thumbsup.jpg

Sử dụng protocol tối ưu lần 3

Tạo 1 protocol Synchronizable

protocol Synchronizable {
    associatedtype Element
    var items: [Element] { get }
    func synchronize(completion: Action)
}

Tạo thêm 1 protocol TableViewSectionDataSource

protocol TableViewSectionDataSource {
    var sectionName: String { get }
    var rowCount: Int { get }
    subscript(i: Int) -> String { get }
}

Tạo các data provider đảm nhiệm việc lấy song theo từng artist

class TaylorSwiftSongDataProvider: Synchronizable {
    typealias Element = SongModel
    private(set) var items = [SongModel]()

    func synchronize(completion: Action) {
        AppServices.getSongByArtist("TaylorSwift", failureHandler: { (reason, errorMessage) in
            print(errorMessage)
            }, completion: {[weak self] songs in
                if let songs = songs {
                    self?.items.removeAll()
                    self?.items.appendContentsOf(songs)
                    completion()
                }
            })
    }
}

extension TaylorSwiftSongDataProvider: TableViewSectionDataSource {
    var sectionName: String {
        return "Taylor Swift"
    }

    var rowCount: Int {
        return items.count
    }

    subscript(i: Int) -> String {
        return self.items[i].trackName!
    }
}
class WestlifeSongDataProvider: Synchronizable {
    typealias Element = SongModel
    private(set) var items = [SongModel]()

    func synchronize(completion: Action) {
        AppServices.getSongByArtist("Westlife", failureHandler: { (reason, errorMessage) in
            print(errorMessage)
            }, completion: {[weak self] songs in
                if let songs = songs {
                    self?.items.removeAll()
                    self?.items.appendContentsOf(songs)
                    completion()
                }
            })
    }
}

extension WestlifeSongDataProvider: TableViewSectionDataSource {
    var sectionName: String {
        return "Westlife"
    }

    var rowCount: Int {
        return items.count
    }

    subscript(i: Int) -> String {
        return items[i].trackName!
    }
}

Ở viewController ta khai báo thêm 1 mảng chứa các data provider trên

var tableViewSections = [TableViewSectionDataSource]()

Tạo 1 hàm add các data provider

func addTableViewSection<T: TableViewSectionDataSource where T: Synchronizable>(section: T, updatingText: String) {
        self.tableViewSections.append(section)

        if let preSyncTask = self.syncTask {
            let index = self.tableViewSections.count - 1
            let updateProgress: Action = { [unowned self] in
                let progress = Float(index) / Float(self.tableViewSections.count)
                self.statusProgressView.progress = progress
            }

            self.syncTask = preSyncTask |>
                            switchToMainThread |>
                            updateStatus(updatingText) |>
                            updateProgress |>
                            section.synchronize
        } else {
            self.syncTask = switchToMainThread |>
                            updateStatus(updatingText, reload: false) |>
                            section.synchronize
        }
    }

Đến đây ta có thể viết lại hàm lấy song như sau:

func getDataSong() {
        addTableViewSection(TaylorSwiftSongDataProvider(), updatingText: "Download TaylorSwift Song ...")
        addTableViewSection(WestlifeSongDataProvider(), updatingText: "Download Westlife Song...")

        guard let performSync = self.syncTask else {
            return
        }

        let resetProgress: Action = {
            [unowned self] in
            self.statusProgressView.progress = 0.0
        }

        let endSync: Action = switchToMainThread |>
                              resetProgress |>
                              updateStatus("Done!")
        let task: Action = performSync |>
                           endSync
        task()
    }

Để lấy thêm song từ 1 artist khác ta chỉ việc tạo thêm 1 data provider cho artist đấy rồi add thêm vào hàm getDataSong.

Ở tableView ta cũng không cần if else theo từng section

extension ViewController: UITableViewDataSource {
    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return tableViewSections.count
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tableViewSections[section].rowCount
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
        let tableViewSection = tableViewSections[indexPath.section]
        cell.textLabel?.text = tableViewSection[indexPath.row]
        return cell
    }

    func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return tableViewSections[section].sectionName
    }
}

Tài liệu tham khảo

http://fuckingswiftblocksyntax.com

http://fuckingclosuresyntax.com

Demo

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


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.