つい最近、社内ハッカソンでFlutterを用いたアプリ開発を行いました。その時に、FlutterやReactなどにおける、State ManagementやApplication Architecture(というか設計手法?)について色々と議論しました。
その中で、Flutterでよく使われているBlocパターンと、自分が昔経験したReactive MVVMが非常に似ており、Blocパターンが採用しているStreamを用いたState管理について良さを説明してくださいといわれたけど、うまく説明できませんでした。
ということで、今回の記事ではStreamを用いたstate管理の利点をまとめたいと思います。
- はじめに
- アプリケーション設計の基本
- Streamを使う利点
- オセロゲームはその入力を受け付けるべきか?
- ネットワーク対戦型オセロゲームでも、シングルプレイでもBusiness Logicのinputは同じ
- その入力は同期処理?非同期処理?
- まとめと欠点
はじめに
今回の記事は、個人的な感想を述べていることとかなり一般的な範囲に拡大したアーキテクチャ設計について述べているため、結構散らかった内容になっていると思います。
また、BlocやReactive Extensionを用いた開発経験がある人を前提としています。
アプリケーション設計の基本
まず大前提として、アプリケーションを設計する場合の基本を整理します。と言っても、そんなに大したことではなく、下図が示す通り、アプリケーションを設計する場合はView(UIだったりDBだったり)とModel(Business Logicとも呼ばれる)を分離することが大事です。
理由もよく言われる通り、UIはそれを表示する何らかのプラットフォームの作法に縛られてしまいますが、Modelなどの純粋なロジックはそういった制約とは関係がないためです。
つまりStreamによるState管理とは、StreamによるBusiness LogicのState管理のことです。
Streamを使う利点
- 値の受け渡しについて、渡す側は受け取る側のことを一切考える必要がない。
- data input, data outputを整理できる。
- 同期、非同期な処理を全て非同期処理で統一できる。
ざっくりとこの3つがStreamを使う利点と考えています。以下では、オセロゲームアプリで具体的な例を示してみたいと思います。
オセロゲームはその入力を受け付けるべきか?
例えば、ユーザが画面をタップした時を考えます。この時、その入力に対してアプリが反応すべきか?入力を受け付けるべきか?ということを考える必要があります。
図に示した通り、入力を受け付けるべきかどうかの理由はプラットフォーム側が関心を持つべき部分と、Business Logic側が関心を持つべき部分に分かれます。
Streamを使うとこの関心部分を綺麗に分けることができます。
Streamの(というかどちらかというとReactive Extensionの考え方かも)使い方の基本としては以下になります。
- データの送り側(Publisher)は受け取りてのことは考えずに、変更があればStreamに流せば良い。
- データの受け取り側(Subscriber)は流れてきたデータを準備ができてから受け取れば良い。
そのため、Business Logicとしては、画面から渡されたデータに反応して、変更がある時だけ画面に変更を通知すれば良いです。つまり、値を返すときに画面がrendering中なのかなどの、画面の事情を一切考慮する必要がなくなります。
また、画面側もユーザ操作や他inputデバイスから受け付けたデータをただStreamに流せば良いです。Business Logicとして無効なデータ(オセロでいえば、ひっくり返せない位置においたか?など)は一切考える必要はありません。それらは受け取りてのBusiness Logicが判断して、有効な入力であれば変更された盤面の情報がStreamを介して流れてくるからです。
ネットワーク対戦型オセロゲームでも、シングルプレイでもBusiness Logicのinputは同じ
次に、少し拡張したオセロゲームを考えます。ネットワーク対戦のオセロゲームと、AIと対戦するシングルプレイのオセロゲームでは、Busness Logic部分は変わるのでしょうか?
StreamによるState管理を行なっている場合は、これは同じBusiness Logicで実現できます。
「オセロの駒を置く」というInput StreamをBusiness Logic側が用意するだけです。あとは、そのStreamに対してデータを流すのが、UIからのイベント(つまりはユーザタップ)なのか、ネットワークの通信結果なのか、はたまた内部で実装したAIロジックからなのかというだけです。
誰が石を置くか?ということはBusiness Logic側は考える必要はありません。ただ、単純に自分が用意した「石を置く」というStreamから流れてきたデータに従って反応を返せば良いだけです。
つまり、「石を置く」というInput Stream1つを用意するだけで、あらゆる入力に対応でき、Business LogicをInput デバイスから疎にすることができます。
その入力は同期処理?非同期処理?
アプリを開発すると、入力のほとんどは非同期処理が要求されます。
非常にシンプルで簡易なアプリであれば、全てを同期処理に変換してしまう方法もあります。そのような場合は、Streamを用いたState管理は煩雑なだけと感じるでしょう。
そのようなシンプルではなく、もっとインタラクティブにUIからの入力を受け付けるようなアプリを作る場合はStreamを用いた非同期処理は非常に有効です。
特に、Business Logicに対するinputとoutputをStreamにすることで、もともと同期処理だったものも非同期処理へと変換できます。つまり、Streamを用いると全てが非同期で並列に処理可能なものへと変換できるのです。
アプリ開発で一番めんどくさいのは、同期処理と非同期処理が混在したロジックを考えないといけない時です。これが全て非同期処理になるのであればかなり見通しが良くなります。
まとめと欠点
このように、StreamによるBusiness LogicのState管理を行う、もっと詳しくいうと、Business LogicのinputとoutputをStreamを介して行うことで、プラットフォームが要求する複雑な事情をBusiness Logicから切り離すことが可能です。
Business Logicは自分が用意したinput用のStreamをただ監視すればよく、また、自分が用意したoutput用のStreamにただ変更内容を流せば良くなります。
あとは、いつユーザの入力を受け付けるべきか?いつ変更内容を画面に反映すべきか?といった複雑な事情はUI側のロジックで処理すれば良いのです。
ただし、Streamは利点だけがあるわけではありません。前述したように、Streamを介することで全てが非同期処理になります。そのため、プログラマがアプリの動作を推定するのが難しくなり、デバッグも複雑になってしまいます。
このような欠点から、何でもかんでもStreamを使うのが良いというわけではありません。シンプルなアプリであれば、シンプルなママの構成を保ったほうがよく、そのようなアプリにはStreamによるState管理はオーバースペックでしょう。
Streamによる利点も十分あるが、その恩恵を十分に得られるアプリなのかを考えて使うかどうかを決めると良いと思います。