RxSwiftを触り始めて2週間ほど経ちました。
近頃はObservable型を使った複数処理の連結実行が便利!?と感じています。
上手く使えば、処理同士を疎結合に保ちながら連結実行できるようになり、
保守性向上に大きく寄与しそうです。
今回は、その辺りを少し整理して書いてみたいと思います。
(例によって試したら動いた!レベルなので他の良い方法があるかも......です)
1. この記事で解決したい課題
まず、今回の記事で解決したい課題を書いてみます。
「指定したURLにリクエストを投げて、返却されたHTMLデータをファイル保存する」
という処理をRxSwiftのObservable型を使って綺麗に実装することを考えます。
この処理を実装しようとした場合、まず以下の2つの処理を分けて実装し、
それらを組み合わせる方式で実現しようとするのではないでしょうか?
- 引数で渡されたURLにリクエストを投げて結果を返却する
- 引数で渡されたデータをファイルに書き込む
実際にコードで書いてみます。
/**
サーバにGETリクエストを投げて結果を返却する
(引数で渡されたURLにリクエストを投げて結果を返却する)
*/
func fetchHtmlData(url: NSURL) {
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let task = session.dataTaskWithURL(url, completionHandler: { [unowned self] (data, response, error) -> Void in
if error != nil {
// エラーハンドリング
} else {
// データ操作処理
self.writeTextFile(data!)
}
})
task.resume()
}
/**
データをテキストファイルに書き込みます。
(引数で渡されたデータをファイルに書き込む)
*/
func writeTextFile(data: NSData) -> Bool {
let fileManager = NSFileManager.defaultManager()
let documentPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
let filePath = documentPath[0] + "/" + "log.txt"
return fileManager.createFileAtPath(filePath, contents: data, attributes: nil)
}
一見、上のコードで綺麗じゃん!と思いがちですが、
fetchHtmlData関数とwriteTextFile関数の結びつきがやや強い印象です。
writeTextFile関数は別の場所から呼び出せるので流用できそうな予感ですが、
fetchHtmlData関数は完全にwriteTextFile関数とベッタリ蜜月な関係です。
これでは「やっぱり処理結果はサーバに送信しよう!」といった
突然かつ無責任な仕様変更が発生したときに詰んでしまいます。
理想としては、fetchHtmlData関数はその名の通りリクエストデータ取得に注力し、
それ以降の作業は誰かに移譲できるようにしたいところです。
この課題をRxSwiftで上手く解決しようというのが今回の趣旨です。
2. 仲介役としてのObserver
fetchHtmlData関数とwriteTextFile関数の関連を断ち切るための手段として
RxSwiftが提供しているObserver(仲介役/Rx的には非同期データストリーム)という考え方を活用します。
fetchHtmlData関数とwriteTextFile関数の間に仲介役を置いてやることで
それぞれの関数は互いに無関心な状態となり、結合度が下がるといった塩梅です。
仲介役は実行先の関数の処理状況(完了/エラー等)を適宜教えてくれる仕様なので
前の処理の完了後に別の処理を実行する...なんてことも可能です。
上手く使うと、図のようにそれぞれの関数は綺麗に独立した形になります。
そのため、仮にHTMLデータの保存方法が突然変わったとしても、fetchHtmlData関数の実装に影響はなく、
組み合わせる関数をすげ替えれば済むといった楽ちん対応が可能になります。
オマケに各関数のユニットテストも書きやすくなって一石二鳥です。
3. RxSwiftを使ったObserverの実現
では、実際にObserverを使った実装パターンを考えてみます。
考えてみます...とは言ったものの結構単純に実現できたりします。
ポイントを整理してみます。
- 関数の戻り値をObservable<返却したいデータ型>に統一する
- 戻り値(関数の実処理含む)は、create関数を通して作成する
- 通知関数(onNext / onError / onComplete)で状況を適宜通知する
実際にコードを見てみます。
/**
サーバにGETリクエストを投げて結果を返却する
*/
func fetchHtmlData(url: NSURL) -> Observable<NSData> {
return create{ observ -> Disposable in
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let task = session.dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
if error != nil {
// エラー発生をObserverを介して通知
observ.onError(error as! ErrorType)
} else {
// 取得したデータをObserverを介して通知
observ.onNext(data!)
// 処理完了をObserverを介して通知
observ.onComplete()
}
})
task.resume()
return NopDisposable.instance
}
}
/**
データをテキストファイルに書き込みます。
*/
func writeTextFile(data: NSData) -> Observable<Bool> {
return create { observ -> Disposable in
let fileManager = NSFileManager.defaultManager()
let documentPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
let filePath = documentPath[0] + "/" + "log.txt"
let result = fileManager.createFileAtPath(filePath, contents: data, attributes: nil)
// 処理判定
if result {
// 処理完了をObserverを介して通知
observ.onComplete()
} else {
// エラー発生をObserverを介して通知
let error = NSError(domain: "サンプル", code: 0, userInfo: nil)
observ.onError(error as ErrorType)
}
return NopDisposable.instance
}
}
create関数の内部に以前の処理が包まれたような感じなので
非RxSwiftからの移行もそこまで骨が折れないのでは?と思っています。
また、注目したいポイントは、fetchHtmlData関数からのwriteTextFile関数呼び出しが
なくなり依存関係を断ち切れたという点ですね。これで結合度が一気に下がりました。
ただ、結合度は下がったっぽいけど、これ本当に連携できるの?という点が心配ですね。
次の章で実際に呼び出してみたいと思います。
4. Observerを組み合わせて実行する
では、実際にObserverを実行してみます。
let viewModel = ViewModel()
let bag = DisposeBag()
@IBAction func pushSomeButton(sender: AnyObject) {
// fetchHtmlData関数を実行
let task = viewModel. fetchHtmlData(url)
task.flatMap { [unowned self] data in
// fetchHtmlDataの結果(data)を引数に次の関数に繋げる
return self.viewModel.writeTextFile(data)
}.subscribeCompleted {
// 完了時の処理
}.addDisposableTo(bag)
}
flatMap関数を使って、Observable型を返却する別関数に連携しています。
もちろん、個々の関数はお互いを呼び出すことない状態です。
ちなみにwriteTextFile関数自体は同期処理として実装していますが
実際はfetchHtmlData関数と同じスレッドで動くので非同期処理になります。
(※RxSwiftの仕様上、指定がなければ前の処理のスレッドを引き継ぐようです)
そのため、挙動しては最初に挙げたfetchHtmlData関数→writeTextFile関数の参照関係
ガチガチのコード例と同じ振る舞いをしつつも、結合度は低いといったコードが実現できました。
5. あとがき
RxSwift...というか、Reactive Programmingなかなか奥深いです。
慣れれば取り回しが良い飛び道具的なライブラリになりますのでオススメです。
またある程度知識が溜まったら記憶の整理がてら記事を書いてみたいと思います。