Reactive Extensionsを使用して複数のサービス非同期コールを連続して呼べるか試してみた。その3

neueccさんよりTwitterでアドバイスを頂きました。

連鎖のフロー的に前なら匿名型、時間軸的に前ならScan、どうにもならない時は外部変数。外部変数を使うと、同時に動かしたり複数に分割したりリトライしたりに不都合が出やすく、やれることに制限がかかるので基本的には避けたい。

簡にして要を得た内容です。私もさらっとこんなことをつぶやけるようになりたいものです。
それはさておき、匿名型を使ってチェーン間の変数引き渡しに挑戦してみます。

アジェンダ

  • テスト用サービスメソッド
  • 匿名型とは
  • 匿名型で次以降のメソッドに値を伝播する
  • ヘルパー用拡張メソッド
  • 匿名型と拡張メソッドを使用した非同期サービスコール
  • 匿名型を使用した方法の雑感
  • まとめ

テスト用サービスメソッド

サーバサイドに用意するサービスメソッドは、その1を参照してください。

匿名型とは

実際にコードを書く前に匿名型のおさらいです。クラスを自分で定義しなくても使える便利な機能です。Linqで重宝します。

var a = new
{
    // int型の読み取り専用プロパティ
    No = 1,

    // double型の読み取り専用プロパティ
    Price = 100d,

    // DateTime?型の読み取り専用プロパティ。
    // nullをセットしたいときは、コンパイラに型を指定するために
    // nullをキャストすればよい。
    Date = (DateTime?)null
};

// 読み取り専用のため、以下のコードはコンパイルエラー
// a.No = 2;

詳しくはMSDNをご覧ください。

匿名型で次以降のメソッドに値を伝播する

では、実際にその2のコードを匿名型で値を引き渡すように書きなおしてみます。サービスからの戻り値を匿名型にセットするためにSelectメソッドを使用します。

Observable.Defer(() =>
{
    // TODO : 何をトリガーにこのメソッドが呼び出されるのか不明
    client.GetAAsync("てすとその3");

    // Selectでイベントの結果を匿名型インスタンスに変換する。
    return getACompleted.Select((IEvent<GetACompletedEventArgs> e) =>
    {
        if (e.EventArgs.Error == null)
        {
            // サービスの戻り値を匿名型にセットして、次のメソッドに渡す。
            return new
            {
                GetAResult = e.EventArgs.Result
            };
        }
        else
        {
            // SubscribeメソッドのonErrorに渡すために例外をスロー
            throw e.EventArgs.Error;
        }
    });
})
.SelectMany(value =>
{
    System.Diagnostics.Debug.WriteLine("Client1. call GetBAsync");

    // 匿名型で保持した値を取り出す。
    string keyB = value.GetAResult;
    client.GetBAsync(keyB);

    System.Diagnostics.Debug.WriteLine("Client2. return getBCompleted");
    return getBCompleted;
})
/****以下略****/

ヘルパー用拡張メソッド

毎回Select内でErrorプロパティチェックを行うのは無駄なので、拡張メソッドを用意します。

public static class ServiceClientExMethods
{
    // メソッド名を何とかしないと・・・
    public static IObservable<TOther> MySelect<T, TOther>(this IObservable<IEvent<T>> source, Func<IEvent<T>, TOther> selector)
        where T : AsyncCompletedEventArgs
    {
        return source.Select((IEvent<T> e) =>
        {
            if (e.EventArgs.Error == null)
            {
                return selector(e);
            }
            else
            {
                throw e.EventArgs.Error;
            }
        });
    }
}

匿名型と拡張メソッドを使用した非同期サービスコール

以上の結果を踏まえ、最終的には下記のようなコードになります。拡張メソッドを使用するとチェーン自体はだいぶすっきり書けます。

Observable.Defer(() =>
{
    // TODO : 何をトリガーにこのメソッドが呼び出されるのか不明
    client.GetAAsync("てすとその2");

    // Selectでイベントの結果を匿名型インスタンスに変換する。
    return getACompleted.MySelect((IEvent<GetACompletedEventArgs> e) =>
    {
        // サービスの戻り値を匿名型にセットして、次のメソッドに渡す。
        return new
        {
            GetAResult = e.EventArgs.Result
        };
    });
})
.SelectMany(value =>
{
    System.Diagnostics.Debug.WriteLine("Client1. call GetBAsync");

    // 匿名型で保持した値を取り出す。
    string keyB = value.GetAResult;
    client.GetBAsync(keyB);

    System.Diagnostics.Debug.WriteLine("Client2. return getBCompleted");
    return getBCompleted.MySelect((IEvent<GetBCompletedEventArgs> e) =>
    {
        // サービスの戻り値を匿名型にセットして、次のメソッドに渡す。
        return new
        {
            GetAResult = value.GetAResult,
            GetBResult = e.EventArgs.Result
        };
    });
})
.Subscribe(value =>
{
    string result = value.GetBResult;
    System.Diagnostics.Debug.WriteLine("Client5. 非同期サービスコールチェーン終了 = " + result);
},
(Exception ex) =>
{
    // サービスコール時のエラー処理
    MessageBox.Show(ex.Message);
});

匿名型を使用した方法の雑感

メソッド変数を使用する方法に比べて、匿名型を使用するとスコープがチェーン内で完結するようになります。これにより、バグの混入を防ぐ効果が期待できます。
一方、匿名型は若干コードが長く・読みにくくなります。しかし、このデメリットは、メリットに比べると無視して良いのではないと思いました。

まとめ

Reactive Extensionsを使用して複数のサービス非同期コールを連続して呼べとき、サービスの戻り値を匿名型で次以降のメソッドへ伝播させていく方法を試してみました。