LoginSignup
63
67

More than 3 years have passed since last update.

まだ closure で消耗してるの?

Last updated at Posted at 2015-12-05

これはなに

入門編です。
非同期処理を実現するためには closure (Objective-C なら blocks) や delegate、observer などがあると思いますが、
これらを Rx で置き換えましょう。
なお、比較対象として closure と RxSwift に絞ります。これは私の体感として、Swift ではだいたい closure を使う場面が多いかなと思うからです。

環境

実現したいこと

ある整数を引数に渡したら非同期でその数を10倍して返してくれる 構造体A があり、それの結果をいろいろいじりたい、そういうのを考えます。
closure でやるといわゆる「コールバック地獄」に陥る事態も Rx ならばスッキリ書ける、という可読性向上を狙います。
モナドとか、関数型とか、そういう計算科学的なことは扱いません。

closure おさらい

上記のことを closure で実現しようとすると以下になります。

Closure
struct closureA {
    func request(num: Int, response: (Int) -> ()) {
        response(num * 10)
    }
}

let a = closureA()
a.request(1) {
    print($0)    // 10
}
a.request(5) {
    print($0)    // 50
}

RxSwift で書く

同じように RxSwift を使って書いてみます。Observable<Element> を使います。

RxSwift
struct observableA {
    func request(num: Int) -> Observable<Int> {
        return just(num * 10)
    }
}

let a = observableA()
a.request(1)
    .subscribeNext {
        print($0)    // 10
    }
    .dispose()
a.request(5)
    .subscribeNext {
        print($0)    // 50
    }
    .dispose()

just(element: Element) -> Observable<Element> であり、ここで Observable を生成しています。
そして subscribeNext で成功した時の値を取り出しています。
これならまだ closure で書いたほうがいいですね。

返ってきた結果を整理したい

Int で返ってきたものを String に変換する

swift
let closure = closureA()
closure.request(1) {
    let str = String($0)
    print(str)               // "10"
}
swift
let observable = observableA()
observable.request(1)
    .map { String($0) }
    .subscribeNext {
        print($0)            // "10"
    }
    .dispose()

1以上の自然数だけを String に変換して出力したい

swift
let closure = closureA()
closure.request(1) { num in
    guard num > 0 else {
        return
    }
    let str = String(num)
    print(str)              // "10"
}
swift
let observable = observableA()
observable.request(1)
    .filter { $0 > 0 }
    .map { String($0) }
    .subscribeNext {
        print($0)
    }
    .dispose()

…どうでしょう?後処理が多くなればなるほど closure の中身が肥大化していきますね。すると本当に欲しいものがどこにあるか見つけづらくなっていくことかと思います。メソッドに切り出して後処理を投げるという手もありますがスコープを外れてしまうので可読性は?というところがあります。
一方 Rx の方は一番欲しいものである subscribeNext の中身は一切変わることなく、filter, map (flatMap もあります) でチェインすることで情報を整理することができます。

コールバック地獄

さて話を変えます。
実践的な非同期処理では、「ある API を叩いた結果を使って別の API を叩く」や、「複数の API を同時に叩く」といったことがあります。
特に前者のような場合では「closure の中に closure を記述する」ということになり、非常に階層が深くなってしまいます。

swift
apiRequest.getUser("swift") { userData in
    apiRequest.follow(userData.id) { result in
        switch result {
        case .Success:
            print("follow success!")
        case .Failure:
            print("something is wrong...")
        }
    }
}

これを Rx の機能を使うとスッキリします。

swift
apiRequest.getUser("swift")
    .map { userData in
        apiRequest.follow(userData.id)
    }
    .switchLatest()
    .subscribe (
        onNext: {
            print("follow success!")
        },
        onError: {
            print("something is wrong...")
        },
        onCompleted: nil,
        onDisposed: nil
    )
    .dispose()

エラーかどうかの分岐もわざわざ switch を使わなくても subscribeonNext:, onError: に振り分けることができるのも便利です。

実践編(Alamofire を Rx にしてみよう)

Swift のネットワークライブラリではもはやデファクトと言ってよい Alamofire
これは標準では closure で結果を返すようになっています。

swift
Alamofire.request(.GET, "https://httpbin.org/get")
         .responseJSON { response in
             debugPrint(response)
         }

README より)

これを Observable で返すようにします。

まずはリクエストのためのインターフェースを切ります。

swift
func call(method: Alamofire.Method, url: String) -> Observable<AnyObject>

<AnyObject> の部分は SwiftyJSONArgo を使って JSON 型にしてもよいでしょう。

実装します。

swift
func call(method: Alamofire.Method, url: String) -> Observable<AnyObject> {
    return create { observer -> Disposable in
        Alamofire.request(method, url)
            .responseJSON { response in
                switch response.result {
                case .Success(let value):
                    observer.on(.Next(value))
                    observer.on(.Completed)
                case .Failure(let error):
                    observer.on(.Error(error))
                }
        }
        return AnonymousDisposable { }
    }
}

これで HTTP リクエストで Rx を使うことができます。

まとめ

このように Rx を取り入れることでより読みやすいコードを書いていきましょう。

63
67
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
63
67