ninjinkun's diary

ninjinkunの日記

アプリ開発と状態遷移の管理

このエントリーは読者としてスマートフォンアプリ開発者とWebフロントエンドエンジニアを想定して書いています。

CROSS2016に出るので、最近の自分の考えを整理しておく。

最近ReduxのSwift実装であるReSwiftを使って開発している。使った感想なども最後の部分に書いたけれど、このエントリーの本題はアプリの状態管理の話。

アプリは大きなシングルトン

iOSAndroid共にアプリを実装しようと思うと大抵シングルトンが必要になる。各ViewController内をまたがってデータを共有したいというユースケースが多いからだ。例えば

  • ユーザーのログイン情報を集約するUserManager
  • コンテンツへのいいね情報を集めるLikesManager
  • ブックマーク情報を集めるBookmarkManager

などなど。もちろんアプリの内容によってこれらの顔ぶれは違ってくると思うけれど、大抵UserManager以外にも一つ二つはシングルトンが存在していることと思う。これらはどのViewControllerから呼びたいし、状態を一意に保ちたいものたちだ。DIでインスタンス化を制御することはできるが、結局内部的にはシングルトンである。

自分はシングルトンを増やすことには抵抗があった。シングルトンは様々なところから使われ、内部状態が常に書き換わっていく。これが増えていくと、結果的にアプリの内部状態がどうなっているのか把握できなくなっていく。今UserManagerがどんな状態にあるのか(初回の起動なのか?ログインしているか?ユーザー情報の更新は終わったか?)を想像しながらコーディングやデバッグをすることになり、これは結構なストレスである。もちろん今までこれをずっとやってきているわけだが…。

しかしアプリ内でデータを共有したい、データを一意に保ちたいというニーズはなくならない。であれば、アプリは全体が大きなシングルトンであると考えてしまったほうがいいのではないか。ReduxのStoreがそんな思想で作られているかは知らないが、結果的にはそういう作り方を強制される。自分にはこの制約は結構しっくりきている。

内部状態の管理

野放図に作られるシングルトンは更新のための規約を持たない。どこからでも呼び出してメソッドを叩けば更新される。また、更新や状態の読み出しのためのメソッドの命名も実装者依存なので、結果的にはregisterUser, isLoggedIn, addLikeなどの名前が増えて行く(実装者にしても別に名前を混乱させる意図はなく、オブジェクトに注目して命名すれば自然とこうなると思う)。

f:id:ninjinkun:20160304001715p:plain ReSwiftのREADMEから転載

Fluxでは変更をActionに集約するという強い制約がある。すべてのActionはなんらかの状態更新を行うために発行されるので、ActionをLoggerに流していればアプリの起動時からの状態更新をすべて追うことができる。今の所は時々デバッグに使っているくらいなのだが、これをクラッシュ時に送信するというアイデアもあるようなので試してみたいと思っている。

別の効能としては、内部状態が実装者である自分の制御下にあるという、ある種の全能感が得られていてとても気分が良い。状態に起因する不具合があっても、必ず追いかけられるという安心感はとても心を落ち着かせてくれる。

感情的な部分を取り上げたが、内部状態が制御下にあるということは、開発メンバーが加わったり変わったりした際に開発がスケールすることに繋がるはずだ(まだその場面に直面していないので断言はできないけれど)。その辺りは大勢でステートルフなGUIをメンテナンスするという問題に直面したFacebook発祥のフレームワークだなという印象である。規約に沿って書けば誰でも内部状態が管理できる仕組みになっている。

状態遷移の管理

結局の所、ある程度複雑化したクライアントサイド開発とは、状態遷移をいかに管理するかということに尽きるのではないかという気がしている。ユーザーからの入力、サーバーからのGETやPOST、様々なイベントがアプリの内部状態を変化させる。このイベントとそれによって起こる更新処理をどうモデリングして実装に落とすのかでアプリの複雑さは変わってくる。モデリングというと偉そうだが、要するに状態をすべて書き出してその遷移を記述するということである。

もちろん方法はFlux系だけではない。例えば状態遷移をステートマシン(有限オートマトン、FSM)として記述するというアイデアもある。今やっている開発の中でReSwiftと並行してSwiftStateも試してみたのだが、ログイン処理などはステートマシンで記述すると読みやすく書けそうだった。ステートマシンを使う手法はゲーム方面ではよく使われていそうなイメージがある(未経験なので詳細は不明)。

状態遷移の管理という観点で捉えると、Flux、ステートマシン、MVVM、Promise、FRP対象としている状態の大きさの違いなのかもしれないとも思う。Fluxはアプリという大きな単位で状態を管理するためのアプローチである。Promiseはもっと小さな単位の状態遷移、ステートマシンはその中間くらいというイメージを持っている。PromiseやObservableはまとめることでより大きな状態遷移を作ることもできるので、小さな状態遷移を積み重ねて大きな状態遷移を作るものとして考えても面白い。

おわりに

増え続ける状態と、その遷移をどうモデリングして管理していくかというのがアプリ開発の課題であると考えている。

しかし状態遷移をすべて書き出すプログラミングは、今までのように状態遷移を意識しないでプログラムを書いていた時よりも大変になる。自分がReSwiftを使っていて感じている問題を以下に書き出しておく。

  • コードの量が増える
    • 特にアラートなど、一時的な状態をViewが持っていたケースを状態に落とそうと思うと大変
  • アニメーションの扱いが大変
    • アニメーションには時間の概念があるが、Reduxにはない
    • コールバックがアニメーション中でも何度も呼ばれる
    • 真面目に扱おうと思うとアニメーションの開始、終了もActionにする必要がある?
  • iOSにはVirtual DOMに相当するものがないので、差分更新を手動でやる必要がある
    • 大抵のViewは構築コストが低いので問題ないが、TableView, CollectionViewは差分更新の仕組みを作った
    • VDOM相当のものが欲しければComponentKit使えとなりそう
  • イベントの連鎖を扱う仕組みがない
  • 個別の状態をsubscribeする仕組みがない
    • subscribeすると自分が関与しないActionでもコールバックが飛んでくる
    • この制約がsubscriberにステートレスな実装を強要している気もするので、悪いわけではないのかも
    • これを解決したDeltaというのもあるけど、どうなんでしょ

現在進行形で実験している最中なので、特に今すぐReSwiftいいよと勧めるつもりはないけれど、この方向性はしばらく掘ってみたい。規模の大きいアプリを構築するために、状態を管理する仕組みが必要というのは今後も出てくる話題だと思う。

ブックマークコメントへの返信

アプリ開発と状態遷移の管理 - ninjinkun's diary

シングルトンが状態を持つというのは悪夢のシナリオでは?

2016/02/03 01:05

アプリ開発と状態遷移の管理 - ninjinkun's diary

それ、シングルトンではなくて、グローバル変数では?スコープがグローバルであることは、シングルトンの要件では無いような……

2016/02/03 09:09

仰るとおり、このエントリではシングルトンをグローバル変数と同義で使っています。言いたかったのはグローバルな状態+手続きなので、リポジトリと言った方が正確だったかもしれません。ただ自分の経験から言うと、クライアントではこの種のリポジトリはシングルトンとして実装されるケースが多かったので、クライアント開発者がイメージしやすい言葉を使いました。

3/30追記

ReSwiftを使ったアプリ「RIDE」がリリースされました。このエントリーで考察したことが実装に反映されているアプリになります。見た目は普通ですが、ウォッチリストの同期部分などを見て頂けるとReSwiftの恩恵がわかるかもです。

4/15追記

ReSwiftについて発表した資料を公開しました。

2017/5/17追記

その後Reduxで小さなアプリを作る実験をやってました。個人アプリだと要件は合わないと思います。