Developer's Blog

Rx時代の先にあるもの

 

こんにちは、アプリケーション共同開発部のみなみです。

iOSアプリ開発を始めてから様々なライブラリを使ってきました。その中で特に強力でおもしろいと感じたのが、Rx (Reactive Extensions)に影響を受けたReactiveCocoaや、RxのSwift実装であるRxSwiftです。Rxライブラリとそれが実現するリアクティブプログラミングは、アプリ開発を大きく変えました。この記事では普段の開発で感じたRxライブラリの威力や課題、そして未来について書きたいと思います。

Rxライブラリは何を変えたのか

イベント通知の統一

あるViewでボタンがタップされたことを、別のViewに伝える方法を考えます。従来はDelegate、シンプルなコールバック関数、NotificationCenter、あるいは自作したシングルトンを使うなど様々な通知方法がありました。それをRxライブラリでは以下のように表現できます。

// ボタンタップの流れを表すclickStreamオブジェクトを生成
let clickStream = button.rx.asObservable()

// clickStreamの参照を、ボタンタップを通知したいViewに渡す
clickStream
    .subscribe { print("click") } // 別のViewでボタンがタップされるたびに、subscribe内が実行される

NotificationCenter?Delegate?何も迷う必要はありません。ボタンタップイベントの流れ(ストリーム)を表す1つのオブジェクト(Observable)を、別のViewに渡すだけ。持ち運びは自由自在です。Rxライブラリによりイベントや値の流れが、1つのオブジェクトとして扱いやすくなりました

フラグ変数、深いネストを一掃して見通しが良くなった

Rxライブラリを使い始めて気づくのが、フラグ変数(状態変数)をほとんど見なくなることです。2つあるボタンの両方がタップされたことを判定する場合を考えます。

従来の方法

@IBOutlet weak var buttonA: UIButton!
@IBOutlet weak var buttonB: UIButton!

var isButtonATapped = false
var isButtonBTapped = false
var isButtonABTapped = false

override func viewDidLoad() {
    super.viewDidLoad()

    buttonA.addTarget(self, action: #selector(buttonATapped), for: .touchUpInside)
    buttonB.addTarget(self, action: #selector(buttonBTapped), for: .touchUpInside)
}

@objc private func buttonATapped() {
    isButtonATapped = true
    buttonTapped()
}

@objc private func buttonBTapped() {
    isButtonBTapped = true
    buttonTapped()
}

private func buttonTapped() {
    if isButtonATapped && isButtonBTapped && !isButtonABTapped {
        isButtonABTapped = true
        
        print("buttonA, buttonB tap")
    }
}

ButtonA、ButtonBがタップされたら、少し離れたところでそれぞれに紐付くフラグ変数を変更します。その後、少し離れたbuttonTappedでButtonAとButtonBの両方がすでにタップされているか確認します。タップが両方されていたら、両方タップ用のフラグ変数isButtonABTappedを変更してから処理を実行します。これで両方のボタンがタップされた時点で、目的の処理が1度だけ実行されることになります。

Rxを使う方法

@IBOutlet weak var buttonA: UIButton!
@IBOutlet weak var buttonB: UIButton!

let disposeBag = DisposeBag()

override func viewDidLoad() {
    super.viewDidLoad()
    
    let buttonAStream = buttonA.rx.tap.asObservable()
    let buttonBStream = buttonB.rx.tap.asObservable()
    
    let buttonABZipStream = Observable.zip([buttonAStream, buttonBStream]) // [[aButtonTap1, bButtonTap1], [aButtonTap2, bButtonTap2], ... ]
    
    buttonABZipStream
        .take(1)
        .subscribe(onNext: { _ in
            print("buttonA, buttonB tap")
        })
        .disposed(by: disposeBag)
}

buttonAStream, buttonBStreamと、それぞれタップの流れをオブジェクトで表現します。それを元に新しい流れを表現するbuttonABZipStreamを作ります。buttonABZipStreamが表現するのは、buttonAStream, buttonBStreamという2つの流れの組み合わせです。最初に流れてきた組み合わせだけ欲しいのでtake(1)します。最後にイベントを受けるためsubscribeすれば、両方のボタンが押された時に処理を実行できます。

多くのフラグ変数が必要で、処理も色々なところに分散しがちなのが従来の方法でした。当然これは処理の読みにくさ、追いにくさに繋がります。本来そこにあるはずのイベントの流れ(ボタンタップの流れ)を読み取ることを難しくするからです。Rxならイベントや値の流れを分かりやすく表現でき、流れの操作も簡単にできます。

フラグ変数だけでなく深いネストも一掃できます。

データを取得するためにAPIをリクエストし、返ってきたデータを元にもう一度APIをリクエスト、最後に返ってきたものを別スレッドで加工する場合

従来の方法

requestApi1() { data1 in
    requestApi2(data1) { data2 in
        processData(data2) { result in
	    // 結果を受け取る
	}
    }
}

requestApi1がrequestApi2の処理を内包し、そのrequestApi2がprocessDataの処理を内包し・・・非同期処理が追加されるたびにどんどんネストが深くなり、それに伴いぱっと見ではどういう処理を行ってるのか把握が難しくなってきます

Rxを使う方法

requestApi1
    .flatMap { data1 in requestApi2(data1) }
    .flatMap { data2 in processData(data2) }
    .subscribe { result in
        // 結果を受け取る
    }

ネストがかなり浅くなりました。requestApi1、requestApi2、processDataのそれぞれが視覚的には独立した処理となり、処理全体も把握しやすくなりました

複雑な非同期処理を分かりやすく表現

Rxが最も力を発揮するのが複雑な非同期処理でしょう。アプリには様々なイベントや値の流れ(Stream)が存在します。ボタンタップの流れ、タイマーなどによる時間の流れ、バックグラウンドイベント・フォアグラウンドイベントの流れ、TextFieldに入力される文字の流れ、など数え切れないほどの流れです。流れはRx以前の時代にも存在しました。ボタンタップというイベントの流れもその1つでしょう。多くのフラグ変数や分散した処理に覆われて見えなかっただけです。そして、様々な流れを加工して新たな流れを作ったり、複雑に組み合わせる必要があるのが複雑な非同期処理です。複雑な非同期処理を多数のフラグ変数や深いネスト、分散して追いにくい処理なしに分かりやすく表現できる。それが従来の方法とRxの一番の違いです。

Rxライブラリの課題

依存度の強さ

Rxライブラリは全てのことを可能にしました。

  1. 通信結果、DBからのデータ取得のため関連するモデルにObservableを返させるようする
  2. Observableを受け取ったViewModelは、Observableから取得した値をViewへの反映用に加工する
  3. iOSには他のプラットフォームのようなデータバインディングがないので、加工済みの値を通知するObservableをViewに渡す
  4. Viewは受け取ったObservableから値を取得し、それを元にUIを更新する
  5. 必要であればイベント通知のためのObservable(もしくはObserver)を共有する

イベント通知も、単純な非同期処理も、複雑な非同期処理も、全てたった1つのライブラリで実現できます。あらゆるモデルが、あらゆるレイヤーが1つのライブラリで繋がります。全てがRxに覆われた世界の完成です。様々な通知方法も、フラグ変数も、コールバッグ地獄もそこには存在しません。Rxのもたらす生産性は圧倒的です。

ただ依存度の強さは普通のライブラリとは次元が違います。Rxライブラリはラップすることが難しいライブラリです。通信、データベース、ログ出力などのライブラリを使う場合、たいてい適当なクラスなどでラップして使います。ライブラリへの直接の依存を減らし入れ替え可能にするためです。例えば通信ライブラリAlamofireを使う場合、以下のようなAPIClientクラスを作ります。通信処理が必要な場所ではAlamofireライブラリを直接使うのではなく、それをラップしたAPIClientを使うことで、ライブラリを変更することになっても影響範囲を最小限に抑えることができます。

class APIClient {
    func send(request: Request, callback: (Result) -> Void) {
        // Alamofire使う処理
    }
}

ではRxライブラリの場合はどうすればいいのでしょうか?試しに以下のように頑張ってRxライブラリをラップしてみます。

ラップされる前

func fetchData(_ data: Data) -> Observable {
    return processData(data)
}

fetchData(data)
    .subscribe { newData in
        // newDataを使う処理
    }

受け取った引数(data)を元に、新たなデータを非同期で生成するObservableをfetchDataメソッドは返します。

RxライブラリのObservableを外部に返していますが、これをこのメソッドの中だけに限定(ラップ)します。

ラップした後

func fetchData(_ data: Data, callback: (Result) -> Void) {
    processData1(data)
        .subscribe { newData in
	    callback(newData)
	}
}

fetchData(data) { newData in
    // newDataを使う処理
}

Observableを返す代わりにコールバック関数を実行するようにしました。

ここでfetchDataの結果(newData)を元に処理を非同期で実行する必要が出てきました。(fetchData2とfetchData3)

fetchData(data) { newData in
    fetchData2(newData) { newData2 in
        fetchData3(newData2) { newData3 in
            // newData3を使う処理
        }
    }
}

どんどんネストが深くなってきます。これでは様々な通知方法やフラグ変数、コールバック地獄などに悩まされていたRx以前の時代に逆戻りです。リアクティブプログラミング実現のためには、オブジェクトの関係自体をRxライブラリに依存させる必要があります。それをラップしてオブジェクト間を特定のライブラリに依存しないようにしても、Rxライブラリの力を100%引き出すことはできません

このようにRxライブラリはとても依存度が強いライブラリです。一旦使い始めると簡単にやめることはできません。同じ系統のライブラリに変えること(RxSwiftからReactiveCocoaなど)さえ困難を極めます。一方でiOSアプリ開発はどうしてもそれに強く依存してしまう事情があります。iOSはUIとのバインディングにRxライブラリを使うことが多いです。他の言語にあるような非同期処理を読みやすくする表現は、Swiftには少なくとも“今は”ありません。そして、GUIアプリケーションは非同期処理の塊です。ボタンはあらゆるタイミングでクリックされます。通信結果はいつ返ってくるかわかりません。ミリ秒の差がユーザー体験に大きな影響を与えるため、メインスレッドで重い処理をすることはできません。
どうしても広い範囲でたった1つのライブラリに依存してしまいます。

イベントを実行することの影響が予測できない

内部の実装を読んでいて感じるのは、Rxライブラリは決して魔法ではないということです。内部には様々なフラグ変数(スコープはライブラリ内に限定されています)が確かに存在します。コールバック関数や、それをラップしたオブジェクトを使ったライブラリ内部での複雑な操作(これはコールバック地獄が少し形を変えたものにも見えます)が確かに存在します。Rxライブラリが実現するのは魔法ではなく、従来外部にあった複雑さ(フラグ変数などの状態変数、コールバック地獄)のライブラリ内への隠蔽です。Rxライブラリを使えば、スコープが広すぎるフラグ変数やコールバック地獄を表面的には解決できます。しかし、それ以外の全てを解決してくれるわけではありません

例えばRxライブラリは、アプリの状態変化の複雑さまでは解決しませんでした。Rxライブラリではmap、flatMap、 reduce、do、subscribeなどのメソッドを通して、Observerに様々なクロージャが登録できます。引数を変換して値を返し、それ以外に余計なことをしない分かりやすいクロージャ(純粋な関数)を登録できる一方で、そうでない分かりにくいクロージャ(副作用のあるもの。クロージャ内に影響が限定されないもの)も制約なく登録できます。

例えばスコープの広い状態(様々な所で共有されている)をキャプチャしているクロージャです。

subject
    .map { // スコープの広い状態 }
    .flatMap { // スコープの広い状態 }
    .do { // スコープの広い状態 }
    .subscribe { // スコープの広い状態 }

Observer(subject)が実行され、登録されたクロージャ(map、flatMap、do、subscribeを通して)が実行されると広い範囲に影響があります。データベースの操作など、ローカルの環境を大きく変えるクロージャについても同じです。
Observerに登録できるクロージャには何の制約もありません。分かりやすいクロージャも、実行されることの影響が分かりにくいクロージャも、Observerの内部には種々雑多なクロージャが存在することができます。
目の前のObserver(以下のコードではPublishSubject)が内部にもつクロージャに何の制約もない以上、それを実行(onNext)することの影響は予測できません

// 中に何が入ってるか分からないObserver
subject.onNext(?)

// subjectに入ってるもの
subject
    .map { // 影響範囲(このクロージャ外に大きな影響)の大きい処理 }
    .flatMap { // 影響範囲の大きい処理 }
    .do { // 影響範囲の大きい処理 }
    .subscribe { // 影響範囲の大きい処理 }

加えてObservableが自由自在に合成できること(とても強力な機能ですが)は、さらに何が入っているのかを分かりにくくします

let mergeSubject = Observable.merge(subject, subject2)
mergeSubject
    .filter { // いろいろ }
    .reduce { // いろいろ }
    .subscribe {}

// subjectに追加で入ったもの
subject
    .filter { // いろいろ }
    .reduce { // いろいろ }

Rxライブラリはありとあらゆる複雑さを隠蔽してくれます。隠蔽される複雑さはフラグ変数やコールバック地獄だけではありません。前述したように、登録されたクロージャがキャプチャしているスコープの広い状態や、それが実行されることによるアプリの複雑な状態変化も隠蔽されます。イベント通知方法の統一は、アプリの状態変化を分かりやすく表現することを意味しません。複雑さの隠蔽は、複雑さの管理を意味しません。Rxライブラリはとてもよくできているため、全てのことが解決するように使用者を錯覚させてしまいます。もしかすると、この辺りにRxのコードがカオスになりやすい原因があるのかもしれません。

高い学習コストが割りに合わない部分がある

例えばAPI処理を一度呼ぶだけなど、単発の結果を受け取るだけの場合です。

requestApi()
    .subscribe { result in
        // 結果を受け取る
    }
    .disposed(by: disposeBag)

subscribeでクロージャを登録して、結果が返ってくるとそれが実行されるだけ、と読めるのはRxライブラリに慣れている人だけです。そうでない人には、まずObservableとは何か、subscribeとは何か、disposeとは何か、さらにHot・Coldなど知る必要のあることが多くあります。目的は非同期で受け取った結果をもとに何かする(できればネストが深くならずに)、ただそれだけです。解決したい問題の単純さに対して、その解決方法があまりに複雑すぎるのではないでしょうか。これは前述したイベント通知に関しても同じことが言えます。

課題への解決策

現在Rxライブラリの使われ方は大きく分けて以下の4つに分類できます。

  • イベント通知の統一手段
  • 単純な非同期処理(特に返す値が単発)
  • 複雑な非同期処理
  • UIとのバインディング手段

このうちイベント通知の一部と、単縦な非同期処理についてはRx以外のものに代替することができます。UIとのバインディングについては代替までは行きませんが、依存度は減らすことができるので紹介したいと思います。

async/await

Swiftの作者からasync/awaitの導入が提案されました。もしこれが実際に導入されると単純な非同期処理はasync/awaitで代替できます。例を見てみましょう。

1つめのAPIをリクエスト、その結果を元に2つめのAPIをリクエスト、その結果を別のスレッドで加工して値を返す場合

Rxを使う方法

func fetchData() -> Observable
    print("start function") // 1

    let observable = requestApi1() // 2
        .flatMap { data1 in self.requestApi2(data1) } // 3
        .flatMap { data2 in self.processData(data2) } // 4
        .subscribe { result in
            // 結果を受け取る
        }
        .disposed(by: disposeBag)

        print("end function") // 5

        return observable
}

async/awaitを使う方法

func fetchData() async -> Result {
    print("start function") // 1

    let data1 = await requestApi1() // 2
    let data2 = await requestApi2(data1) // 3
    let result = await processData(data2) // 4

    print("end function") // 5

    return result
}

async/awaitを使った例の方が少しだけネストが浅くなってます。特定のライブラリやプログラミングスタイルにも依存しません

  • await以降の処理には進まず、そこで中断する(重要なことですが呼び出し元スレッドをブロックするわけではありません)
  • awaitの処理が完了後、中断した処理を再開

処理としてはたったこれだけです。解決したい処理の単純さに対して十分割りに合う学習コストです。またasync/awaitは中立です。そこにObservableもHot、Coldも存在しません。そのためメソッドの使用者に特定のライブラリやプログラミングスタイルを強制することはありません

そして最も重要なのが非同期処理が直感的に読めるようになることです。まるで普段読んでいる文章を読むかのように自然な流れで読めるようになるのです。

プログラムと文章は似ています。大小あるにせよ意味のある塊(プログラムなら関数、構造体、クラスなど。文章なら文、段落、章)があり、それを組み合わせて作りたいものを完成させます(ストーリーに矛盾があると破綻するところも似ていますw)。文字の誕生から数千年の間、様々な文章が生み出されました。おそらくその全てに共通するのが、一定方向に文の流れが進むということです。縦書きの日本語なら右から左、横書きなら上から下、英語も上から下。書いた順に読み進めることができます今まで人は習慣としてずっとそうしてきました

そしてプログラムという文章は“基本的には”上から下に進みます。一部の厄介な例外を除いて、、、。

その例外とはもちろん非同期処理です。非同期処理の実行される順番は、人の直感にかなり反したものになります。
Rxライブラリを使った方法(従来の方法でも)では、1, 2, 3, 4, 5と処理が書いてあるのに対して、実際に処理が実行される(クロージャを登録する処理を除く)順番は1, 2, 5!, 3!, 4!です。関数という意味のある塊の最後の処理まで実行された後、少し上に戻って3, 4が実行されます。処理が書かれている順番と、実際に実行される順番に大きな不一致があります。これはプログラムを流れるように読むことを難しくします

一方、async/awaitを使うと1, 2, 3, 4, 5と書いてある順に処理が実行されます。これにより他の同期的な部分と同じように、上から下に流れるようにプログラムを読むことができます。この魔法のような表現方法は、おそらくSwiftでもコンパイラがasync部分を元にステートマシンを生成して実現します(予想)。ライブラリに押し付けていた複雑さを、コンパイラに押し付けただけと言われればそれまでなのですが、少なくとも人が読み書きするSwiftの世界では非同期処理の複雑さを大きく減らすことができるのです。

async/awaitはコンパイラの力を借りて実現する、これ以上ない強力な非同期処理の表現方法です。これでRxライブラリで実現していた非同期処理の全てを代替することはできません。しかし本来同期的でない非同期処理を、同期的に読み書きできる部分が少しでも増えるのはとても画期的なことです。

Redux

以前Reduxに関する記事を書きました。この記事では言及しなかったのですが、Redux(ReSwift)によりRxへの依存を部分的に減らすことができます。1つはイベント通知の中でも、アプリの状態変化を通知するものです。

  1. ユーザが何らかのアクション(ボタンタップなど)を起こす
  2. それをModel、Presenter、ViewModelなどに伝える
  3. その先で、通信処理、データベースの操作、状態の変化(様々なスコープの)などが行われる
  4. 最後に変化後の状態をViewに反映する

これがアプリの1つのサイクルです。このサイクルは何度も繰り返されます。

ここでCounterという増えたり減ったりする状態があります。Modelにあるのかもしれません。Presenterかもしれません。あるいはViewModelかもしれません。とにかくCounterはどこかに状態としてあります。incrementCountSubjectを実行するとその状態がどこかで変化します。変化後の状態はcounterStreamに通知され、それを元にViewが更新されます。簡単なコードとしては以下のようになります。

counterStream: Observable
incrementCountSubject: PublishSubject

// ボタンがタップされた時に実行されるメソッド
func buttunTapped() {
    // どこかでCounterの状態が変化(1だけ増加)する
    incrementCountSubject.onNext(1)
}

// どこかにあるCounterの更新処理
incrementCount
    .subscribe { increment in
        self.counter += increment
    }

// どこかにあるCounterの状態が変化するとsubscribe内が実行される
counterStream
    .subscribe(onNext: { newCounterState in
        // Counterの新しい状態をViewに反映する
    })

ボタンがタップされIncrementCountイベントが実行(incrementCountをonNext)される。どこかにあるCounterの状態が変化(増加)。新しいCounterの状態が通知(subscribe)される。最後にそれをViewに反映する。

これはReduxで以下のように書けます。

// ボタンがタップされた時に実行されるメソッド
func buttunTapped() {
    // どこかでCounterの状態が変化(1だけ増加)する
    store.dispatch(IncrementCountAction(increment: 1))
}

// どこかにあるCounterの更新処理
func counterReducer(action: Action, state: AppState?) -> AppState {
    var state = state ?? AppState()
    
    switch action {
    case let action as IncrementCounterAction:
        state.counter += action.increment
    default:
        break
    }
    
    return state
}

// どこかにあるCounterの状態が変化するとnewState内が実行される
store.subscribe(self)
func newState(state: AppState) {
    // Counterの新しい状態(stateに含まれる)をViewに反映する
}

ボタンがタップされIncrementCountイベントが実行(IncrementCountActionをdispatch)される。どこかにあるCounterの状態が変化(増加)。新しいCounterの状態が通知(newState)される。最後にそれをViewに反映する。

とても似ています!2つの違いはどこにあるのでしょうか。

例えばIncrementCountイベント実行後に他のカウンター(Counter2)の状態を変化させる、増加量を2倍にする、データベース操作する場合についてです。どこかにあるCounterの状態を更新する部分では以下のような処理が追加されます。

Rxの場合

incrementCount
    .do { increment in self.counter2 += increment } // 1
    .map { increment in increment * 2 } // 2
    .do { self.updateDatabase() } // 3
    .subscribe { newIncrement in // 4
        self.counter += newIncrement
    }
  1. do 自分の外の状態(Counter2)を変更する分かりにくいクロージャ
  2. map 引数の値を2倍にして返すだけ。それ以外に余計なこと(クロージャ外の状態変えたり)しない分かりやすいクロージャ
  3. do データベースを操作しローカルの環境を大きく変える分かりにくいクロージャ
  4. subscribe 自分の外の状態(Counter)を変更する分かりにくクロージャ

Rxでは登録したクロージャは分かりやすいもの、そうでないもの、明確な区別なくObserverに保持されます。また今回はmapを分かりやすいクロージャにしましたが、
map内でスコープの広い状態の変更やデータベース操作などをすることで、分かりにくいクロージャにすることもできます。この辺は制約が無いため、Rxを使う人によってかなり幅がでます

Reduxの場合

// 1
func counter2Reducer(action: Action, state: Int?) -> Int {
    var state = state ?? 0
    
    switch action {
    case let action as IncrementCounterAction:
        state += action.increment
    default:
        break
    }
    
    return state
}

// 2 
func updateDatabase() -> Store.ActionCreator { // もしくはAsyncActionCreator
    return { state, store in
        // データベースの操作
        
        return ?
    }
}


// 3
func counterReducer(action: Action, state: Int?) -> Int {
    var state = state ?? 0
    
    switch action {
    case let action as IncrementCounterAction:
        state += action.increment * 2
    default:
        break
    }
    
    return state
}
  1. Reducer 引数を元に新たなCounter2の状態を作って返すだけの分かりやすい関数
  2. ActionCreator(理想はMiddleware)データベースを操作しローカルの環境を大きく変える分かりにくいクロージャ
  3. Reducer 引数を元に新たなCounterの状態(増加量2倍で更新された)を作って返すだけの分かりやすい関数

Reduxは明確です。Reducerは分かりやすい関数(純粋な関数)、Middlewareは分かりにくいクロージャ(副作用のある)と明確に区別されてReduxのStore内に保持されます(ActionCreatorは保持されませんが、、、)。外から見てもReducerはその関数に書かれていることが全て、それ以外(Middleware, ActionCreator)はそうではない明確に判別できます。実行順も明確です。ActionCreator、Middlewareの分かりにくい処理が実行されてから、Reducerにある分かりやすい処理が全て連続的に実行されます。またActionを発行することでのみ状態を変更できるという制約がReduxにはあります。これは状態の変化を比較的分かりやすく表現します。

依存度の強さも大きく違います。例ではdo、map、subscribeというRx独自のメソッドの一部が、ReduxではReducerになっています。Reducerはただの値型の引数を受け取り、ただの値型を返すだけ、それ以外に余計なことをしないただの関数です。特定の何かのライブラリに強く依存したものでもありません。Middlewareを除けばRedux内に存在するのは、値型とただの関数で行われるただのSwiftプログラミングだけです。

Reduxでは状態変化に関する通知を統一できるのに加え、状態管理も実現できるのです。特定のライブラリやプログラミングスタイルに強く依存することもありません。

View側でも依存箇所を少なく抑えることができます。

ViewModelのようなものを作る場合、それが持つプロパティをViewに反映させるためRxライブラリを使います。プロパティはそれぞれ対象のViewにバインドされます。プロパティが多ければ多いほど依存箇所は増えます。

viewModel.title
    .subscribe(onNext: { text in
        // Viewに反映
        ? = text
    })
        
viewModel.content1
    .subscribe(onNext: { text in
        // Viewに反映
        ? = text
    })
        
viewModel.content2
    .subscribe(onNext: { text in
        // Viewに反映
        ? = text
    })
        
viewModel.content3
    .subscribe(onNext: { text in
        // Viewに反映
        ? = text
    })

Reduxは1つのメソッド(newState)にアプリ全体の状態が値型として返ってきます。それを元にView側でどこを更新するか考えます。モデルの状態をViewに反映するための依存箇所は、1箇所(newState)で済みます。

struct AppState: StateType {
    var title: String
    var content1: String
    var content2: String
    var content3: String
}

func newState(state: AppState) {
    // Viewに反映
    ? = state.title
    ? = state.content1
    ? = state.content2
    ? = state.content3
}

Rxは大きくて複雑なライブラリです。それに対してReudxは全て合わせても500行程度、一日で十分読める量で構造もシンプルです。複雑なライブラリに依存している部分を少しでも減らせるのは大きなメリットです。

このようにRxと比較して、状態変化を分かりやすく表現(管理)できる、依存度が弱いなどのメリットがあるのがReduxです。ただメリットばかりではもちろんありません。Rxに比べれば学習コストは高くないですが、決して低いわけでもありません。Rxのようになんでもできるわけではないので、代替できる部分も限られます。また非同期処理はReduxのReducerで扱えないので、MiddlewareやActionCreatorに頼る必要があります。その場合ActionCreatorではなく、Middlewareに非同期処理を集めるのが理想です。ただSwiftにはredux-sagaのような強力なMiddlewareがないので、実際にはActionCreatorに多くを頼ることが予想されます。そしてReduxはアプリ全体の構造にも関わるので、既存アプリに導入するコストは高いといえます。

未来

Rxは一つの時代を築きました。一方で依存度の強さ、学習コストの高さ、状態管理の概念(制約)が無いなどの課題もあります。今回はその解決策としてasync/await、Reduxを挙げました。これは今まであまりに多くの役割を負っていたRxとの役割分担を可能にします。他にも自分が思いつかなかった代替方法があるのかもしれません。もしかすると、将来的にiOSにも他のプラットフォームにあるようなバインディングの仕組みが導入されるかもしれません。またあえてRx一本で行く選択肢もあります。デメリットもありますが、全てRxで実現することでとても高い生産性を得られるのもまた事実です。
重要なのは選択肢が増えることです。Rxの時代は終わりません。とても便利なライブラリであり、何よりとてもおもしろいライブラリです。やはりおもしろいプロダクトを作るためには、使っていておもしろいかどうかは重要です。ただ、全てがRxでないと実現できない時代は終わろうとしていると感じます。

Rxライブラリは、自身が本来一番得意とする複雑な非同期処理に集中する。それ以外の部分は、それをRxライブラリ以上に得意とするライブラリや方法に任せる。上手いこと役割が分散された、そんな緩やかで賑やかな未来を自分は想像しています。

みなさんはどんな未来を想像しますか?

参考

 

フェンリルのオフィシャル Twitter アカウントでは、フェンリルプロダクトの最新情報などをつぶやいています。よろしければフォローしてください!

 

Copyright © 2019 Fenrir Inc. All rights reserved.