Reactive Extensions:Cache-AsideおよびRefresh-Ahead戦略を使用した条件付きAPIのクライアント

rx-logo

はじめに



この記事では、条件付きAPIサービス用のクライアントライブラリの開発を検討します。 このようなサービスとして、Habrahabrの架空のRest-apiを使用します。



このような日常的なタスクをもう少し面白くするために、キャッシュを追加して要件を複雑にし、 Reactive Extensionsライブラリですべてを調整します。



猫の下で興味のある人を招待します。



次のjsonを返すnonexisting-api.habrahabr.ru/v1/karma/user_name形式のURLがあるとします。



{ "userName" : "requested user name", "karma" : 123, "lastModified" : "2014-09-01" }
      
      







そのようなサービスに適用し、応答をデシリアライズし、ユーザーに結果を表示することはすべて非常に簡単です。 おそらく、単純な実装は次のようになります。



  public sealed class NonReactiveHabraClient { private IHttpClient HttpClient { get; set; } public NonReactiveHabraClient(IHttpClient httpClient) { HttpClient = httpClient; } public async Task<KarmaModel> GetKarmaForUser(string userName) { var karmaResponse = await HttpClient.Get(userName); if (!karmaResponse.IsSuccessful) { throw karmaResponse.Exception; } return karmaResponse.Data; } }
      
      







キャッシングを追加





モバイルアプリケーションでの作業は、デスクトップアプリケーションまたはWebアプリケーションでの作業とは大きく異なります。 モバイルアプリケーションは、片方の手で「実行中」に使用され、多くの場合、通信状態が悪くなります。 もちろん、ユーザーは関心のある情報をできるだけ速く表示することを期待しています。 明らかに、データをキャッシュする必要があります。



このアプリケーションの主要な機能は、データの更新がめったにないこと、データの鮮度と関連性の重要度が低いことです。 つまり、前回のリリースからの情報を表示する余裕があります。 多くの天気予報アプリケーション、Twitterクライアントなどには、同様の特性があります。



私たちのようなアプリケーションでは、次のロジックが非常に一般的です:

  1. アプリケーションはすぐに起動するはずです。
  2. キャッシュデータを表示します。
  3. バックエンドから最新のデータを取得してください。
  4. 成功した場合、データをキャッシュに保存します。
  5. ユーザーに新しいデータを表示するか、エラーを報告します。




または、同じですが、ダイアグラムの形式です(シーケンスダイアグラムの描画方法を完全に忘れていないことを願っています)。



画像



ローカルアプリケーションキャッシュを操作するには、いくつかの基本的な戦略があります。 この記事では、キャッシュアサイドアプローチ(またはパターン)に興味があります。



このパターンの基本的な考え方は、キャッシュは単なる受動的なデータストレージであるということです。 したがって、データを更新するタスクは、キャッシュを使用するコードの肩にかかっています。 このようなキャッシュの場合、かなり単純なインターフェースを定義できます。



  public interface ICache { bool HasCached(string userName); KarmaModel GetCachedItem(string userName); void Put(KarmaModel updatedKarma); }
      
      







このインターフェイスを実装するキャッシュは、前のリストの要件2および4を適切に満たしています。 項目2、3、および4は合わせて、 Refresh-Ahead Cachingと呼ばれるアプローチのバージョンです。



上記のアプローチは両方のパターンの古典的な実装ではなく、基本的なアイデアの組み合わせにすぎないことに注意してください。 ただし、これにはパターンが存在します。



このアプローチの標準的な反復実装が読者に問題を引き起こさないことを望んでいるので、Reactive Extensionsを使用してすぐにオプションに進みます。 さらに、少なくとも一般的なプレゼンテーションレベルで、読者がRxに既に精通していることを願っています。 Rxに慣れていない場合、またはRxを更新する場合は、 SergeyT 記事「Reactive Extensions」と非同期操作を読むことをお勧めします。



実装





したがって、まずはVisual Studioでプロジェクトを作成し、プロジェクトタイプをクラスライブラリとして指定します。 Rx-Main NuGetパッケージが必要になります。



 Install-Package Rx-Main
      
      







httpクライアントの抽象化を定義します。



  public interface IHttpClient { Task<KarmaResponse> Get(string userName); } public class KarmaResponse { public bool IsSuccessful { get; set; } public KarmaModel Data { get; set; } public Exception Exception { get; set; } } public class KarmaModel { public string UserName { get; set; } public int Karma { get; set; } public DateTime LastModified { get; set; } }
      
      







http-requestsの実装の特定の実装、応答の解析と逆シリアル化、エラー処理は重要ではありません。



APIクライアントのインターフェースを定義します。



  public interface IHabraClient { IObservable<KarmaModel> GetKarmaForUser(string userName); }
      
      







ここでのキーポイント: IObservable <T> 、つまり、サブスクライブできるイベントの「ストリーム」を返します。



最後に、HabraClientの実装を定義します。



 public sealed class ReactiveHabraClient : IHabraClient { private ICache Cache { get; set; } private IHttpClient HttpClient { get; set; } private IScheduler Scheduler { get; set; } public ReactiveHabraClient(ICache cache, IHttpClient httpClient, IScheduler scheduler) { Cache = cache; HttpClient = httpClient; Scheduler = scheduler; } public IObservable<KarmaModel> GetKarmaForUser(string userName) { return Observable.Create<KarmaModel>(observer => Scheduler.Schedule(async () => { KarmaModel karma = null; if (Cache.HasCached(userName)) { karma = Cache.GetCachedItem(userName); observer.OnNext(karma); } var karmaResponse = await HttpClient.Get(userName); if (!karmaResponse.IsSuccessful) { observer.OnError(karmaResponse.Exception); return; } var updatedKarma = karmaResponse.Data; Cache.Put(updatedKarma); if (karma == null || updatedKarma.LastModified > karma.LastModified) { observer.OnNext(updatedKarma); } observer.OnCompleted(); })); } }
      
      







コードは非常に簡単です。キャッシュデータ(存在する場合)をすぐに返し、更新された値を静かに非同期に要求する新しいObservable



オブジェクトを作成して返します。 データが更新された場合( LastModified



フィールドが変更された場合)、サブスクライバーに再度通知し、データをキャッシュに保存してシーケンスを終了します。



したがって、ReactiveHabraClientを使用したViewモデルのコードはコンパクトで宣言型になります。



  public class MainViewModel { private IHabraClient HabraClient { get; set; } public MainViewModel(IHabraClient habraClient, string userName) { HabraClient = habraClient; Initialize(userName); } private void Initialize(string userName) { IsLoading = true; HabraClient.GetKarmaForUser(userName) .Subscribe(onNext: HandleData, onError: HandleException, onCompleted: () => IsLoading = false); } private void HandleException(Exception exception) { ErrorMessage = exception.Message; IsLoading = false; } private void HandleData(KarmaModel data) { Karma = data.Karma; } public bool IsLoading { get; set; } public int? Karma { get; set; } public string ErrorMessage { get; set; } }
      
      







もちろん、注意深い読者はINotifyPropertyChangedとディスパッチの実装がないことにすでに気づいています( OnNext



OnError



OnCompleted



はUIスレッドにありません)。 お気に入りのMVVMフレームワークがこれらのタスクを引き受けたと想像してください。



おそらくこの記事は完成したかもしれませんが、テストの問題はまったく公開していません。 実際、非同期コードの単体テストを書くことは、しばしばあまり便利ではありません。 Rxを使用した非同期コードについて話すことは何ですか?



テスト中





ReactiveHabraClientとMainViewModelの単体テストを作成してみましょう。



これを行うには、タイプクラスライブラリの新しいプロジェクトを作成し、メインプロジェクトへのリンクを追加して、いくつかのNuGetパッケージをインストールします。

すなわち: Rx-MainRx-TestingNunitおよびMoq



 Install-Package Rx-Main Install-Package Rx-Testing Install-Package NUnit Install-Package Moq
      
      







ReactiveHabraClientTest



から継承したクラスReactiveHabraClientTest



作成します。

ReactiveTest



は、Rx-Testingパッケージに付属する基本クラスです。 テストを書くときに役立ついくつかのメソッドを定義します。



記事に大量のリストを散らかしたり、ここではクラスごとにテストを1つだけ行います。 他のテストはGitHubで利用できます。 リポジトリへのリンクは記事の最後にあります。



次のシナリオをテストします。キャッシュが空の場合、HabraClientはデータをダウンロードしてキャッシュに入れ、 OnNext



OnCompleted



呼び出します。



そのためには、 IHttpClient



ICache



Mockが必要ICache



。 Rx-TestパッケージのTestSchedulerクラスも役立ちます。

TestScheduler



ISchedulerインターフェイスを実装し、プラットフォーム固有のスケジューラー実装の代わりに使用できます。 このクラスにより、文字通り時間を制御し、非同期コードを段階的に実行できます。 Virtual Time Schedulingを使用したRxクエリのテストという優れた記事を強くお勧めします。



 [SetUp] public void SetUp() { Model = new KarmaModel {Karma = 10, LastModified = new DateTime(2014, 09, 10, 1, 1, 1, 0), UserName = USER_NAME}; Cache = new Mock<ICache>(); Scheduler = new TestScheduler(); HttpClient = new Mock<IHttpClient>(); }
      
      







そして、テスト自体の記述を始めましょう。

手配する


モックの動作を設定しましょう:キャッシュは空になり、データは正常にロードされます。

  Cache.Setup(c => c.HasCached(It.IsAny<string>())).Returns(false); HttpClient.Setup(http => http.Get(USER_NAME)).ReturnsAsync(new KarmaResponse { Data = Model, IsSuccessful = true }); var client = new ReactiveHabraClient(Cache.Object, HttpClient.Object, Scheduler);
      
      







テストケースでは、1つのOnNext呼び出しと1つのOnCompleted呼び出しのシーケンスを想定しています。

次のシーケンスを作成します。



  var expected = Scheduler.CreateHotObservable(OnNext(2, Model), OnCompleted<KarmaModel>(2));
      
      





ここで説明が必要です。 OnNext(2, Model)



メソッドは、 ReactiveTest



定義されているメソッドです。

彼の署名は次のとおりです。

 public static Recorded<Notification<T>> OnNext<T>(long ticks, T value)
      
      







基本的に、OnNextメソッドがModelパラメーターで呼び出されたというレコードを作成します。 マジックナンバー2は、 TestScheduler



タイムです。 非常に美しいソリューションではありませんが、理解できます。 「tick」番号0でTestScheduler



を作成し、「tick」番号1でイベントをサブスクライブし、「tick」番号2で一連のメッセージを受信します。



行為




 var results = Scheduler.Start(() => client.GetKarmaForUser(USER_NAME), 0, 1, 10);
      
      







ここでは、ゼロティックを作成し、最初の「ティック」でclient.GetKarmaForUser(USER_NAME)



にサブスクライブするclient.GetKarmaForUser(USER_NAME)



を実行します。 最後のパラメーターはDisposeが呼び出される「ティック」ですが、この場合、この値は重要ではありません。



そして最後に、最後のステップ。



アサート




  ReactiveAssert.AreElementsEqual(expected.Messages, results.Messages); Cache.Verify(cache => cache.Put(Model), Times.Once);
      
      







受信したメッセージシーケンスが意図したシーケンスと一致することを確認します。 また、更新されたモデルがキャッシュに保存されていることも確認してください。



理想的には、このようなテストは2つに分割する必要がありますが、rxの世界ではおなじみの手法が引き続き機能することを実証したかったのです。



MainViewModel



のテストはわずかに異なります。



IHabraClient



モックを作成し、タイプのIHabraClient



を宣言しKarmaStream



  Subject: 
      

[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }








Subject<T> IObservable



IObserver



. KarmaStream



GetKarmaForUser



OnNext



, OnCompleted



OnError



. "" c TestScheduler



.



:

[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }











, GitHub .



Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.







. . , Windows Phone.



.







:

http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/




Subject:

[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }








Subject<T> IObservable



IObserver



. KarmaStream



GetKarmaForUser



OnNext



, OnCompleted



OnError



. "" c TestScheduler



.



:

[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }











, GitHub .



Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.







. . , Windows Phone.



.







:

http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/




  Subject: 
      

[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }








Subject<T> IObservable



IObserver



. KarmaStream



GetKarmaForUser



OnNext



, OnCompleted



OnError



. "" c TestScheduler



.



:

[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }











, GitHub .



Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.







. . , Windows Phone.



.







:

http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/




Subject:

[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }








Subject<T> IObservable



IObserver



. KarmaStream



GetKarmaForUser



OnNext



, OnCompleted



OnError



. "" c TestScheduler



.



:

[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }











, GitHub .



Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.







. . , Windows Phone.



.







:

http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/




Subject:

[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }








Subject<T> IObservable



IObserver



. KarmaStream



GetKarmaForUser



OnNext



, OnCompleted



OnError



. "" c TestScheduler



.



:

[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }











, GitHub .



Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.







. . , Windows Phone.



.







:

http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/




Subject:

[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }








Subject<T> IObservable



IObserver



. KarmaStream



GetKarmaForUser



OnNext



, OnCompleted



OnError



. "" c TestScheduler



.



:

[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }











, GitHub .



Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.







. . , Windows Phone.



.







:

http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/




Subject:

[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }








Subject<T> IObservable



IObserver



. KarmaStream



GetKarmaForUser



OnNext



, OnCompleted



OnError



. "" c TestScheduler



.



:

[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }











, GitHub .



Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.







. . , Windows Phone.



.







:

http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/




Subject:

[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }








Subject<T> IObservable



IObserver



. KarmaStream



GetKarmaForUser



OnNext



, OnCompleted



OnError



. "" c TestScheduler



.



:

[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }











, GitHub .



Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.







. . , Windows Phone.



.







:

http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/




Subject:

[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }








Subject<T> IObservable



IObserver



. KarmaStream



GetKarmaForUser



OnNext



, OnCompleted



OnError



. "" c TestScheduler



.



:

[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }











, GitHub .



Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.







. . , Windows Phone.



.







:

http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/




Subject:

[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }








Subject<T> IObservable



IObserver



. KarmaStream



GetKarmaForUser



OnNext



, OnCompleted



OnError



. "" c TestScheduler



.



:

[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }











, GitHub .



Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.







. . , Windows Phone.



.







:

http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/







All Articles