Help us understand the problem. What is going on with this article?

Rxを使った設計をビジュアル化する

More than 3 years have passed since last update.

はじめに

リアクティブプログラミングでアプリを設計するにはどうすればいいんでしょうか?できるだけ直感的に理解できるようにビジュアル化して解説します。

今まで RxSwift についての以下の記事を書いてきました。

  1. オブザーバーパターンから始めるRxSwift入門
  2. RxSwift入門(2) 非同期処理してみる
  3. RxSwiftを深く理解する
  4. RxSwiftの機能カタログ

まずは既存のイベント通知のやり方を置き換えたり、非同期処理に使ったり、「すぐ使える便利な道具」として RxSwift を使ってもらおうという趣旨で解説しています。

そのため「リアクティブプログラミングとは?」とか「関数型としての側面が〜」とか、オブジェクト指向プログラマにとって敷居が高そうと感じられてしまう言葉や説明を避けてきました。

概念よりもまずライブラリとして使って親しんでからの方が、概念も理解しやすいと思ったのです。RxSwfit は単なる既存技術の置き換え用ライブラリとして使っても十分便利で強力だと感じたのもあります。

そうはいっても Rx はリアクティブプログラミングという新しいプログラミングパラダイムを実現する目的を持って登場してきたわけで、それを知ることでコードや設計を別の視点からも見ることができると思うのです。

これからは徹底的にリアクティブプログラミングでシステムを構築する方法を説明していきます。

説明には RxSwift を用います。Rx とか言っといて特定言語に寄ってしまってすみません。

新しいプログラミングパラダイムを学ぶ

今までと全く違うプログラミングパラダイムを学ぶのは難しいです。「次は関数型プログラミングが来る」と言われて学習した人もいると思いますが、「で?それで今作ってるシステムをどうやって設計すんの?」ってなりませんでしたか?

新しい概念は

  • イメージで捉える
  • 既に慣れ親しんでいるものに喩える
  • 既に慣れ親しんだものを置き換えてみる
  • 具体例

が理解の鍵です1。これらを意識しながら説明していきます。

Rx の各 operator の動作はマーブルダイアグラムでビジュアル化することで理解しやすくなっています。ここではアプリ全体の設計というマクロな視点で、リアクティブプログラミングをビジュアル化することを考えてみます。

リアクティブプログラミングとは?

リアクティブは「変化に反応する」という意味です。よくある説明はこうです。

var a = 1
let b = a + 1
a = 2
print("b = \(b)")

これはどう表示されるでしょうか?

b = 2

ですね。でもちょっと待ってください。そりゃ手続き型に慣れてれば当たり前なんですが、数学的に考えてみれば b = a + 1 なのに、a = 2 になっても b = 2 のままっておかしくない?

じゃあこうしたらどうなるでしょうか?

let a = BehaviorSubject(value: 1)  // a = 1
let b = a.map { $0 + 1 }  // b = a + 1
b.subscribeNext { print("b = \($0)") }
a.onNext(2)  // a = 2

以下のように表示されます。

b = 2
b = 3

b = a + 1 なんだから、a が 2 になったら b は 3 になるはずじゃん。っていうのがリアクティブプログラミングです。

これってとっても「宣言的」だと思いませんか?「b とは a + 1」であると宣言してるわけです。a がどう変わろうと、常に b は a + 1 です。

他によくある説明はエクセルのセルです。他のセルを参照する式を入力しておくと、参照先のセルが変化したとき即座に反映されます。

excel1.png

excel2.png

このように変化に即反応するためには、ようは値に変化が起こったら通知してもらって即反映すればいいんです。オブザーバーパターンを使えば実現できますね。Rx を使うとそれをとても簡潔に記述することができます。

Rx での実装時の記述は確かに「宣言的」です。クラス間のやり取りもオブザーバーパターンで変化を通知してもらうようにすればいいんでしょう。でもシステム全体の設計はイメージできるでしょうか?

で?それで今作ってるシステムをどうやって設計すんの?

LabView

Rx を学んで、学生の時に研究室で少し触った LabView を思い出したんですよ。あれ?これって LabView と似てない?って。

LabView はちょっと衝撃的なプログラミング環境です。テキストでプログラム書きません。ビジュアルなプログラミングを行うグラフィカル言語です。

どんなものか知りたい人は以下を参考にしてください。

Rx での設計をビジュアル化するのには、これを真似したらいいんじゃないかと。なんせグラフィカル言語ですし。

LabView は名前の通り元々は研究室で計測機器を制御する GUI プログラムを作成する目的で開発されています。PC の画面にボタンとか設定値入力するテキストボックスとか、計測データを表示するグラフだとか配置します。それらのGUIからのデータの流れと、計測機器からのデータの流れに処理を適用して、計測機器とGUIとの間の入出力を繋ぎます。

スマホアプリでは「計測機器」は「ネットワーク通信」、「データベースアクセス」などの外部環境とのデータ通信に置き換わるでしょう。

データの流れる配線に対して処理を適用して繋いで・・・ってこれ、Rx そのものに思えませんか?2 データの流れる配線が Observable で、それに処理を適用するのが operator です。

それっぽい雰囲気を感じてもらうために簡単な図を描いてみました。ハードウェア構成は以下の想定です。温度センサーが付いているマイコンボードがあって、パソコンから制御できるようになっています。

hardware.png

で、以下がアプリです。上部がGUIデザインで下部がプログラムです。プログラム部分はデータフローに処理を適用して接続していく形になっています。赤い端子が出力、青い端子が入力を表しています。プログラム部分を Rx で考えてみましょう。

dataflow_concept.png

ボタンが押されるたびに tap からイベントが流れ、running (BehaviorSubject)の最新値をトグルしたものを running に onNext します。running の値を filter して、true ならマイコンボードの start に false なら stop にイベントを通知します。

running から流れてくる値を map で文字列に変換してボタンのタイトルに設定します。true か false かによって「停止」か「開始」になります。

マイコンボードからは温度センサーの出力電圧が流れてきます3。この電圧値を map で℃に変換して、ラベルのテキスト(文字列)と、グラフの最新値(数値)に通知します。

上の図はパワポで書きましたが、以下はより実装を意識した図を UMLet を使って書いてみました4。MVP(MVVM)パターンを取り入れて、画面に表示する内容の管理は Presenter としてビジネスロジック層から分離しています5

dataFlow.png

データフロープログラミング

LabView はデータフロープログラミング言語の一種だそうです。

データフロープログラミング - Wikipedia には何やら難しいことが書いてあります。ポイントはここです。

従来型のプログラムは一連の命令文で構成されているが、データフロープログラムは組み立てラインに労働者が並んでいるようなもので、各労働者は材料が到着したとたんに割り当てられた作業を開始する。データフロー言語が本質的に並列的であるというのはこのためである。各処理・操作には保持すべき隠蔽された状態を持たず、どの処理・操作も同時に実行可能である。

言葉で説明されても分かりにくいので、ビジュアルで分かるように図を交えて説明していきます。

組み立てラインのイメージ

これはマーブルダイアグラムを見れば分かるかと思います。もうちょっとベルトコンベアーっぽい感じにすると、例えば zip のイメージは以下です。Observable がイベントを流すベルトコンベアーで、operator が作業員(英語の意味そのまんまですね)と思ってもらえればいいです。

zip.png

ソフトウェアが現実世界と違うところは複製が容易ということです。ラインを分岐させることができます。

copy.png

// 単に同じObservableを使うだけで簡単に分岐できる
let start = running.filter { $0 }
let stop = running.filter { !$0 }

並列処理が容易

データフロープログラミングの利点に並列処理が容易というのがあります。ビジュアルにして見ると分かりやすいですね。先ほどの温度計測プログラムのデータフロー図で、平行に走ってる配線のデータ処理は並列処理できることが一目瞭然です。

また同じ配線への処理でも、各オペレータの処理は並列実行することができます。

組み立てラインに労働者が並んでいるようなもので、各労働者は材料が到着したとたんに割り当てられた作業を開始する。

労働者とは Rx ではオペレータです。工場のベルトコンベアーで働く各労働者はそれぞれ並列に作業をしていますよね?

map_filter.png

各労働者が同じ道具を共有して利用すると、そこで順番待ちが発生してしまいます。「利用中」カードとか、順番待ちリストとかを用意して管理しないといけません。各労働者が互いに独立して作業できるようにしておけば、排他制御などの面倒なことを考えずに並列化できます(つまり各オペレータの処理を参照透過で副作用がないようにすればいいんですが、それらについては別記事で説明する予定)。

各処理・操作には保持すべき隠蔽された状態を持たず、どの処理・操作も同時に実行可能である。

とはそういうことです。

Rx ではスケジューラを使って各オペレータの並列処理を容易に記述することができます。

let newObservable = observable
  .observeOn(scheduler1)
  .map(parse)
  .observeOn(scheduler2)
  .filter(valid)
  .observeOn(MainScheduler.instance)
  .shareReplayLatestWhileConnected()

ただし並列処理にはオーバーヘッドがかかります。小さな処理を細々と並列処理にするとかえってパフォーマンスが落ちるので要注意です6。「並列処理が容易」ではありますが、「どこをどの単位で並列処理すべきか」の判断は簡単ではありません7

リアクティブプログラミングと何が違うの?

データフロープログラミングとリアクティブプログラミングは、捉え方が違うだけで本質的には同じものに思えます。

私の感覚では、設計時にはデータフロープログラミングとして捉えた方が設計しやすく、実装時にはデータフローとしても考えつつ、リアクティブプログラミングとして捉えて「xx とは yy である」という定義を宣言的にプログラミングしているという感覚もありつつ、という感じです。

オブジェクト指向のクラス分割手法を流用

システムを分割する単位としてオブジェクト指向のクラス分割手法をそのまま流用できます。Rx 自体がオブジェクト指向言語用のライブラリですし、ようするにオブザーバーパターンですので。

オブジェクト指向プログラマに「関数型プログラミングでシステムを設計して」と言われても、どうしたらいいか分からないって状態になるかもしれませんが、Rx での設計でその心配はありません。Rx は関数型プログラミングのスタイルと言えるかと思いますが、設計は今までと同じ考え方から移行できます。

リアクティブプログラミングで設計時に意識すべき重要なポイントは、必要な時にデータを取得しに行くのではなく、常に変化を通知してもらうということです。

以下は典型的なオブジェクト指向設計です。依存を単方向にするために Delegate を使ってイベントを通知しています。値変化は Delegate で通知されますが、初期値だけはプロパティ値を読み出します。

OOP_class_diagram.png

プロパティを Observable にして Delegate を置き換えると以下になります。

OOP_with_Observable.png

この例では全てのメソッドは戻り値がありませんが、もし戻り値のあるメソッドがあるなら、

  • インスタンスメソッド呼び出しの結果出力はプロパティの Observable で通知
  • Delegate メソッドの戻り値はインスタンスメソッド呼び出しに変更

してメソッドの戻り値をなくします。上段が出力、下段が入力にはっきり分かれるようにしておきます。

シンプルになりましたね。BehaviorSubject を使えば初期値も subscribe 時に通知してもらえます。これだけでも十分 Rx を導入するメリットがあります。さらに入力のメソッド呼び出しを Observer として受け取ると以下のようにメソッドがなくなります。

Rx_class_diagram.png

これだと入出力が混ざって分かりにくいので、UML の仕様を無視していいなら、上に出力、下に入力を書いた方が見やすいです。

Rx_class_diagram_in_out.png

オブジェクト指向で適切にクラス分割ができるなら困ることはないと思います。ただしクラス内部の実装方法は、手続き型プログラミングとは全く違ったものになります。

コードにしてみよう

ここまでビジュアル化して説明してきましたが、ここでは具体的にコードで表現してみます。動作確認していないのでバグありありかもしれませんが、あくまでこんなイメージってことで。

Swinject を使って DI する想定です。なんですが、この例ではモックに置き換えてユニットテストはできません。これについては後述します。

import Swinject

// DIの設定
extension SwinjectStoryboard {
  class func setup() {
    let container = defaultContainer

    container.register(Board.self) { _ in
      Board()
    }.inObjectScope(.Container) // Singleton

    container.register(Thermometer.self) { r in
      Thermometer(board: r.resolve(Board.self)!)
    }.inObjectScope(.Container) // Singleton

    container.register(Presenter.self) { r in
      Presenter(thermometer: r.resolve(Thermometer.self)!)
    }

    container.registerForStoryboard(ViewController.self) { r, c in
      c.presenter = r.resolve(Presenter.self)!
    }
  }
}

DI 中にオブジェクトが取得できなかったらそれは設定ミスによるバグなので、即時クラッシュさせるべきです。なので ! でオプショナルを外しています。

import UIKit
import RxSwift
import RxCocoa

final class ViewController: UIViewController {
  private let disposeBag = DisposeBag()

  var presenter: Presenter!

  @IBOutlet weak var startStopButton: UIButton!
  @IBOutlet weak var tempratureLabel: UILabel!
  @IBOutlet weak var graph: GraphView!

  override func viewDidLoad() {
    super.viewDidLoad()

    presenter.buttonTitle.bindTo(startStopButton.rx_title())
      .addDisposableTo(disposeBag)
    presenter.labelText.bindTo(tempratureLabel.rx_text)
      .addDisposableTo(disposeBag)
    presenter.graphValue.bindTo(graph.value)
      .addDisposableTo(disposeBag)

    startStopButton.rx_tap.bindTo(presenter.buttonTap)
      .addDisposableTo(disposeBag)
  }
}

プロパティ注入されるものは注入されなければ設定ミスによるバグです。なので ! を付けて宣言して nil チェックは省略します。もし nil なら画面ロード時に即クラッシュします。

import RxSwift

final class Presenter {
  private let disposeBag = DisposeBag()

  let buttonTitle: Observable<String?>
  let labelText: Observable<String>
  let graphValue: Observable<Double>

  let buttonTap: AnyObserver<Void>

  init(thermometer: Thermometer) {
    buttonTitle = thermometer.running
      .map { $0 ? "停止" : "開始" }
      .observeOn(MainScheduler.instance)
      .shareReplayLatestWhileConnected()

    labelText = thermometer.temprature
      .map { String(format: "%.2f", $0) }
      .observeOn(MainScheduler.instance)
      .shareReplayLatestWhileConnected()

    graphValue = thermometer.temprature
      .observeOn(MainScheduler.instance)
      .shareReplayLatestWhileConnected()

    buttonTap = thermometer.toggleRunning
  }
}

Presenter と ViewController は常に1対1なので、Cold - Hot 変換は必要ないって考えもアリだと思いますが、一応律儀にやってます。ViewController へのイベント通知はここで全てメインスレッドにしています8

import RxSwift
import RxCocoa

private func convertToTemprature(voltage: Double) -> Double {
  // ...
}

final class Thermometer {
  private let disposeBag = DisposeBag()
  private let runningVar = Variable(false)
  private let toggleRunningSubject = PublishSubject<Void>()

  let running: Observable<Bool>
  let temprature: Observable<Double>

  let toggleRunning: AnyObserver<Void>

  init(board: Board) {
    running = runningVar.asObservable()
    toggleRunning = toggleRunningSubject.asObserver()

    temprature = board.voltage
      .map(convertToTemprature)
      .shareReplayLatestWhileConnected()

    toggleRunningSubject
      .withLatestFrom(running) { _, running in !running }
      .bindTo(runningVar)
      .addDisposableTo(disposeBag)

    running
      .filter { $0 }
      .map { _ in () }
      .bindTo(board.start)
      .addDisposableTo(disposeBag)

    running
      .skip(1)
      .filter { !$0 }
      .map { _ in () }
      .bindTo(board.stop)
      .addDisposableTo(disposeBag)
  }
}

図では書いていませんでしたが skip(1) しています。running は subscribe すると現在値を流してきます。初期値は false なので skip(1) しないと初期化時に stop を呼び出してしまいます。

bindTo は Observer に値をそのまま伝達するだけの subscrbe です。Rx に一般的なものではありません。そのためか RxSwift 本体ではなく、iOS/Mac SDK を拡張する RxCocoa で提供されています。これの意味するところは、Presenter と画面部品とのバインドに使うことを想定しているってことだと思います。名前からしても。

でも Observable と Observer を接続するのに便利なんだもん。それ以外の所でも使ってしまいたい。というか RxSwift 本体に入れて欲しい。

import RxSwift

final class Board {
  let voltage: Observable<Double>

  let start: AnyObserver<Void>
  let stop: AnyObserver<Void>

  // ...
}

Board の中身は省略します。これについては後述します。

複数引数のメソッドを置き換える

ここでの例ではメソッドに渡す引数がありません(つまり Void)。渡す引数が1つならいいのですが、複数になってきたらどうやって Observer にするのでしょう?

Java ならクラス化することになるでしょう9。Swift の場合、構造体にするかタプルにする方法が考えられます。

struct DoSomethingArguments {
  let arg1: Int
  let arg2: String
}

final class Hoge {
  // ...

  let doSomething: AnyObserver<DoSomethingArguments>
  let startSomething: AnyObserver<(arg1: Int, arg2: String)>

  // ...
}

Swift の場合、メソッドの置き換えとしてはタプルにするのが自然で良さそうですね。

ユニットテスト可能にする

オブジェクト指向ではポリモーフィズムを使って、ターゲットクラスが利用する他のクラスのモックを用意します。そのためにはオーバーライド可能にする必要があります。

先ほどの例ではまずクラスの final を外す必要があります。また定数はオーバーライドできないので getter を用意する必要があります。Swift なら computed property にすればいいです。

でも継承可能にするのもモッククラス作るのも面倒じゃないですか?こういう方法はどうでしょうか?

import RxSwift

final class Board {
  static let initialVoltage = 0.0

  let voltage: Observable<Double>

  let start: AnyObserver<Void>
  let stop: AnyObserver<Void>

  // ...

#if TEST
  init(voltage: Observable<Double>, start: AnyObserver<Void>, stop: AnyObserver<Void>) {
    self.voltage = voltage
    self.start = start
    self.stop = stop
  }
#endif
}

こんな感じでテスト用のコンストラクタを用意します。全てのインプット(Observer)とアウトプット(Observable)を外から設定できるようにしています。上の例ではテスト時だけコンパイルオプションに TEST が設定される10想定です。

テストする際にはこれらに Subject を渡せばアウトプットにイベントを発生させることも、インプットに発生したイベントを監視することも自由自在です。

import XCTest
import RxSwift

class ThermometerTests: XCTestCase {
  let voltage = Variable(Board.initialVoltage)
  let start = PublishSubject<Void>()
  let stop = PublishSubject<Void>()

  lazy var boardMock: Board = { [unowned self] in
    return Board(voltage: self.voltage.asObservable(),
                 start: self.start.asObserver(), stop: self.stop.asObserver())
  }()

  var target: Thermometer!

  override func setUp() {
    super.setUp()
    voltage.value = Board.initialVoltage
    target = Thermometer(board: boardMock)
  }

  func testInitialRunningIsFalse() {
    var initialRunning = true
    target.running.subscribeNext {
      initialRunning = $0
    }.dispose()

    XCTAssertFalse(initialRunning)
  }

  func testStartWhenRunningTurnsIntoTrue() {
    var count = 0
    let disposable = start.subscribeNext {
      count += 1
    }
    target.toggleRunning.onNext(())
    disposable.dispose()

    XCTAssert(count == 1)
  }

  func testNotStartWhenRunningTurnsIntoFalse() {
    var count = 0
    let disposable = start.subscribeNext {
      count += 1
    }
    target.toggleRunning.onNext(())
    target.toggleRunning.onNext(())
    disposable.dispose()

    XCTAssert(count == 1)
  }

  func testRunningTurnsIntoTrue() {
    var running = false
    let disposable = target.running.subscribeNext {
      running = $0
    }
    target.toggleRunning.onNext(())
    disposable.dispose()

    XCTAssertTrue(running)
  }

  // ...
}

外部とのデータ通信を非同期処理する

Board の実装は省略していますが、ここは USB などの外部ポートを介して通信を行うはずです。これらは非同期通信にした方が良いでしょう。もうちょっとスマホアプリっぽくして、インターネットを通して遠隔の温度を測定すると考えてみましょう。

smartphone_server.png

Board の下に Server クラスを設けてもいいんですが、Board を Server に置き換えます。非同期処理は Observable に包みます。

import RxSwift

final class Server {
  private let startUrl: NSURL
  private let stopUrl: NSURL
  private let voltageUrl: NSURL

  func start() -> Observable<Void> {
    return sendCommandToUrl(startUrl)
  }

  func stop() -> Observable<Void> {
    return sendCommandToUrl(stopUrl)
  }

  func getVoltage() -> Observable<Double> {
    return fetchJsonFromUrl(voltageUrl)
      .map(getVoltageFromJson)
  }

  init(baseUrl: String) {
    startUrl = NSURL(string: baseUrl + "start")!
    stopUrl = NSURL(string: baseUrl + "stop")!
    voltageUrl = NSURL(string: baseUrl + "voltage")!
  }
}

// ...

こんな感じで非同期処理を行う Observable を返すようにします。Thermometer の方はこれらの非同期処理を実行するように書き換えます。非同期実行が完了するまではボタンを無効にできるように starting, stopping プロパティを設けておきましょう11。またボタンタイトルはサーバー通信が成功した時点で変化するようにします。

final class Thermometer {
  private let startingVar = Variable(false)
  private let stoppingVar = Variable(false)

  // ...
  let starting: Observable<Bool>
  let stopping: Observable<Bool>

  init(server: Server) {
    starting = startingVar.asObservable()
    stopping = stoppingVar.asObservable()
    // ...

    toggleRunningSubject
      .withLatestFrom(running) { _, running in !running }
      .flatMapFirst { [startingVar, stoppingVar] newRunning -> Observable<Void> in
        let processing = newRunning ? startingVar : stoppingVar
        let function = newRunning ? server.start : server.stop
        return Observable.using({ () -> AnonymousDisposable in
            processing.value = true
            return AnonymousDisposable { processing.value = false }
          }, observableFactory: { _ in
            function()
          })
      }
      .subscribeNext { [runningVar] _ in
        let previousRunning = runningVar.value
        runningVar.value = !previousRunning
      }
      .addDisposableTo(disposeBag)

    // running = true の間は voltage を一定間隔で取得する
    // (実装は後で説明)
  }
}

toggleRunning にイベントが来ると Server の start か stop を呼び出します。その時に using を使って処理が完了するまで starting / stopping を true にしておきます。処理中にボタンが押されても無視するように、flatMapFirst を使って非同期処理を実行しています。staring / stopping のどちらかが true の間にボタンを disable にするなら起こりえませんが、ここでは UI の実装は関知していないので。

温度を取得する部分は以下のようになります。

  static private let interval = 30.0
  private let intervalScheduler = ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Default)
  // ...

  init(server: Server) {
    // ...

    temprature = Observable<Int>.interval(Thermometer.interval, scheduler: intervalScheduler)
      .withLatestFrom(running)
      .filter { running in running == true }
      .flatMapFirst { _ in server.getVoltage() }
      .map(convertToTemprature)
      .catchError { _ in Observable.empty() }
      .shareReplayLatestWhileConnected()
  }

temprature を監視するものが現れることで動作を開始します。Cold - Hot 変換しているので複数から監視されても実行は1つだけです。interval で一定間隔でイベントを発生させ、その時に running が true なら サーバーから電圧を取得します。flatMapFirst を使っているので、前の処理に時間がかかっている場合は次のリクエストは発行されません。そして取得した電圧を温度に変換しています。

エラー処理は単純に無視するようにしました。catchError で何もイベントを発生させない Observable.empty() を返すことでもみ消しています。

今回は Server が Observable を返すメソッドを提供するようにしましたが、今まで通り Observable / Observer でやりとりするようにしておいて、内部で Observable を生成して実行するようにラップする設計も考えられます。ようは非同期処理 Observable を実行するのを Thermometer 側と Server 側のどちらにするかです。

なぜ入力をObserverにするのか?

RxSwift の RxExample の中にログイン画面のサンプルがあります。これは MVVM パターンに従っていて、ViewController が ViewModel 12にイベントを通知するのに、Observable を渡すという方法を取っています。

この記事では Observable を渡すという方法ではなく、Observer に接続するという方法を取っています。これがなぜかは Presenter オブジェクトの破棄タイミングと購読解除のタイミングを考えるとわかります。

この記事の例では画面遷移がありませんが、画面遷移がある場合は ViewController は必要なくなれば破棄されます。Presenter は ViewController と一緒に破棄されます。

先の例で Presenter が Thermometer に Observable を渡すようにした場合、subscribe するのは Thermometer 側になります。Presenter が破棄されるとき、その subscribe を解除してもらわないと提供した Observable が破棄されません。

final class ViewController: UIViewController {
  // ...

  override func viewDidLoad() {
    // ...

    presenter.buttonTap = startStopButton.rx_tap
  }
}
final class Presenter {
  var buttonTap: Observable<Void>? {
    set {
      thermometer.toggleTap = newValue
    }
  }

  // ...

上のコードで thermometer.toggleTap に渡しているのは結局 startStopButton.rx_tap なので、Thermometer が toggleTap に渡された Observale の購読を解除してくれないと、startStopButton.rx_tap が解放されません。

オブザーバーパターンから始めるRxSwift入門」で、 Variable は自身が解放されるときに onCompleted を発行すると説明しました。購読解除してもらう1つの方法は、このように onCompleted を発行することです。rx_tap はボタンが解放されるときに onCompleted を発行する実装になっているようです。でも全ての Observable がそうなっているかどうかちゃんと把握できるでしょうか?

それとオブジェクト生成時にコンストラクタで Observable を渡せるならいいですが、そうでない場合は設定されるまで nil (null) になるのも嫌です。

RxExample の例は ViewController と ViewModel の生存期間が全く一緒なので問題になりませんが、生存期間が違うクラス間では Observer で入力を受ける方が購読解除の管理も理解も楽です。

一方 Observer で受ける場合のデメリットは、その実体となる Subject を用意しないといけないことです。それでもいちいち

  • クラス間の生存期間が同一かどうか
  • オブジェクトの解放時に所属する Observable が onCompleted を発行するかどうか

を考えてどちらを選択するか決めるより、全部同じ仕組みで統一した方が考えなきゃいけないことが減ってハッピーじゃないですか?13

まとめ

Rx を用いたリアクティブプログラミングのアプリ設計をどうやったらいいかを、データフロープログラミングと捉えてビジュアル化してみました。

また従来のオブジェクト指向でのクラス分割を利用して、それを Rx 対応に変更する方法を示しました。

そしてそれを実際のコードに落とし込んでみました。

クラス分割については従来のオブジェクト指向設計のやり方を使うことができましたが、クラスの内部実装は似ても似つかないものになっています。次はオブジェクト指向でのメソッドの実装が、Rx のオペレータを使ったものにどのように対応しているのか、例をあげて説明しようと思います。

次の記事「オブジェクト指向プログラミングからリアクティブプログラミングへ、そして関数型プログラミングとの関係」へGO!


  1. これがこの記事の内容の上位概念であり、この記事はその例になっています。こういうメタ思考は大事だと思います。より応用範囲が広いです。 

  2. iOSやAndroidの開発ができるグラフィカル言語を作ったら需要ありそう?コンパイラ部分はRxSwiftやRxJavaを使ったコードに変換すれば比較的簡単に実現できそうです。IDE作るのが大変ですが。 

  3. 普通はマイコンボードで温度に変換するんでしょうけど、説明の都合的にはPC側でmapしたいんです。 

  4. UMLの書き方と違っていてもご愛嬌。Swiftに馴染みがない人のために説明すると、$0が流れてくる個々のデータを表しています。結合オペレータであるwithLatestFromは入力データが2つあって、2つ目(この例ではrunning)が$1です。 

  5. buttonTapではなくstartStopButtonTap、labelTextではなくtempratureLabelTextにすべきかと思いますが、長すぎて図が横長になりすぎるのでこの名前にしています。 

  6. iOSにはGCDがあり、幾つスレッドを作って何をどのスレッドで動作させるかを管理する必要はないのでAndroidよりかなり楽です。でも並列処理にオーバーヘッドがあって、細かく分けるとパフォーマンスが落ちることには変わりありません。 

  7. じゃあ小さな処理を細々と並列処理してもパフォーマンスが落ちないようにすればいいじゃんって発想なのがErlangですね。OSのプロセスとは別の超軽量なプロセスを管理する仕組みをVMに用意してあり、並列処理できるものを細かい単位でプロセスとして分割できます。プロセス同士はメモリ共有できないのでそれぞれの間でメッセージ通信させるという仕組みです。Erlangはちょっと古い言語で文法に癖がありすぎるので、それをRubyっぽくしたのが最近話題のElixirです。 

  8. iOSにしろAndroidにしろビューの変更はメインスレッド限定。Presenterはそれより下がどのスレッドでイベントを通知するかは関知しない設計です。 

  9. メソッド呼び出しのたびにヒープにオブジェクトを生成することになるので、パフォーマンス劣化が心配です。 

  10. テストターゲットの Build Settings / Swift Compiler - Custom Flags / Other Swift Flags に -DTEST を追加します。 

  11. 説明に都合がいいのでこれで。本当はrunningと合わせてstatusという1つのプロパティにしてしまった方がシンプルになるかと。enumでStop/Starting/Run/Stoppingの4つの状態を表します。 

  12. ViewModelはPresenterと同じ役割。 

  13. これはSwiftにデストラクタがあるからというのも大きいです。ガーベジコレクタのある言語では、購読解除のタイミング管理が難しくなります。finalizeは使うべきではありません。Androidの場合は、ActivityとPresenterの間では画面表示/非表示のタイミングで接続/接続解除を行い、その他はPresenter含めシングルトンオブジェクトにして購読しっぱなしにするのが楽かと思います。ただし画像などのサイズの大きなオブジェクトをPresenterで(BehaviorSubjectなどを使って)キャッシュするのはやめましょう。 

k5n
好きなことはプログラミングと楽器演奏。そのうち金になる方を仕事に、ならないほうを趣味にしています。 若かりし頃はベンチャーでバリバリやってましたが、一度体を壊してからは忙し過ぎない仕事を探してマイペースにやってます。 C/C++, Swift, Objective-C, Java, Kotlin, Ruby, Python, PHP, TypeScript, Rust
creato
「言われたものを作るだけ」ではない共創型システムパートナーを掲げる名古屋の少数精鋭開発会社。
http://www.creato-c.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした