プロダクトを解析して考えるより良いUniRxの活用方法

AvatarPosted by

この記事は GRIPHONE Advent Calendar 2020 19日目の記事です。

クライアントエンジニアの二間瀬です。
今回は、現在弊社で開発・運用中のプロダクトにおけるUniRxの使用例をご紹介しつつ、より良いUniRxの使用方法について考察していこうと思います。

UniRxの使用状況を解析してみる

まずはプロダクト内におけるUniRxの使用状況を見てみます。今回はRoslynを使用して、オペレータの種類ごとの使用回数を解析しました。解析方法の詳細に関しては、本記事の本題と外れてしまうため割愛させていただきます。
呼び出し回数全2118回のうち、使用回数上位15個を抜粋すると以下のようになりました。

オペレータ名呼び出し回数
Where625
First227
EveryUpdate176
Select138
FromCoroutine118
NextFrame95
Timer91
Zip73
SelectMany70
TakeWhile70
Take56
AsObservable41
Finally35
Skip28
Return26

WhereやFirst、Selectなどよく見かける面子がそろっているのは予想通りでした。
気になったのはFromCoroutineやSelectManyなどで、どうやら非同期処理らしいことをしていそうな気配を感じます。一方で実際にコードを見てみると、非同期処理にも関わらずイベントとみなして記述されている部分も見受けられました。
これらの結果より、主に非同期処理に着目して、実際のコード例を交えながら現在のUniRxの使用方法に改善の余地があるかを見ていきます。

非同期処理を見直す

サーバー通信の同期処理

サーバからのデータ受信を待機するコードを見てみると、主にZipを使用して複数の通信の同期をしていることがわかりました。

(画像引用: http://reactivex.io/documentation/operators/zip.html)

Zipは複数のストリームからのメッセージを待ち受け、それぞれ一つずつメッセージが来たらそれらを一つにまとめて流すオペレータです。プロダクトではこれを、各ゲーム画面の初期化時にリソース読み込み等の完了通知として利用していました。非同期処理とイベント処理が混じっている場合は除きますが、非同期処理のみの場合は、ストリームの長さを考えると適切な使用方法とはいえない場合があります。

非同期処理は、すべてのストリームの長さが1とみなされますので、ZipではなくWhenAllを使う方がより適切である可能性が高いです。
実際、サーバからデータを受け取るためのメソッドではSubjectのFirstを戻り値とするつくりをしているため、現状Zipに流れるメッセージは常に一つずつとなっている箇所がありそうです。

他に、今後async/awaitへ置き換えることがある場合も、WhenAllにしていれば受信側のコードを修正する必要がないという利点もあるため、置き換えが可能な場合は実施してよさそうです。ただ、統一性が損なわれるという面もあるので、ルールが定められているならそれに準拠するべきです。

サーバー通信の受信処理

次にサーバからデータを受信するコードも見てみると、以下のような構造でデータを取得するメソッドを定義していました。本プロダクトでは、このようなコードがしばしば登場します。サーバから受け取ったレスポンスデータを受け取り加工などを行った後、完了通知を意味するSubjectにOnNextを発行します。

IObservable GetSomething() 
{
   var subject = new Subject();
   Api.GetSomething() // 通信本体
      .Subscribe(response => 
      {
          DoSomething(response); // レスポンスの加工、データクラスへの通知など
          subject.OnNext(Unit.Default);
      })
      .AddTo(this); 
    return subject.First();
}

OnNextを一度だけ発行するSubjectを呼び出す度に生成していますが、この部分を次のように置き換えることで、無駄なメモリ消費を抑えられる可能性があります。コード量も少なくなり、すっきりしました。

IObservable GetSomething() 
{
   return Api.GetSomething()
      .Do(DoSomething)
      .AsUnitObservable();
}

ただし、置換前のコードとは異なり、DoSomethingはメソッドの戻り値が購読されないと実行されないという点に注意が必要です。なお、この修正後コードはさらに ForEachAsync()に置換することができる場合があります(Last().Do().AsUnitObservable()とほぼ同じ処理)。

順序関係のある処理

非同期処理の連結のためにSelectManyが比較的多く使用されていますが、上記と同様の理由から、ContinueWithに置換できる場合が多そうです。
ContinueWithはSelectManyの単発版で、一回のみメッセージを送受信する場合は、より軽量に動作するContinueWithに置換することでより最適にすることができそうです。

(画像引用: http://reactivex.io/documentation/operators/flatmap.html)
※FlatMapはSelectManyと等価

おわりに

今回は弊社が運用中のプロダクトを解析した結果をもとにUniRxのより適切な使用方法について考察しました。
UniRxは強力で柔軟なツールですが、それゆえに実現したいことに対する方法が複数通りあることもしばしばあります。
そういった時に最適な方法を選べるようにしていきたいですね。