Unity NCMBをUniRxで扱えるようにしてみた

# はじめに

ニフティクラウド mobile backend（以降NCMB）とは、ニフティさんが提供するmBaaSサービスです。
クラウドサーバ上に会員登録やデータの保存や検索ができるサービスです。

# NCMB

## Unity Plugin

このNCMBはUnity向けのPlguinを提供しており、無料で利用することができるようになっています。

[https://github.com/NIFTYCloud-mbaas/ncmb_unity](https://github.com/NIFTYCloud-mbaas/ncmb_unity)

## NCMB Unity Pluginの問題点

さて、このUnity Pluginなのですが「APIの非同期実行時にコールバックがネストする」という問題点があります。

```csharp:ネストする例
//ログインして成功したらユーザの詳細情報を取得する
NCMBUser.LogInAsync("test_user_id", "test_password", error =>
{
    if (error != null)
    {
        //失敗
        Debug.LogError(error);
    }
    {
        //ログインに成功したらユーザの詳細取得
        var currentUser = NCMBUser.CurrentUser;
        currentUser.FetchAsync(exception =>
        {
            if (exception != null)
            {
                //失敗
                Debug.LogError(exception);
            }
            else
            {
                //成功
                //ユーザの登録されたメールアドレスの表示
                Debug.Log(currentUser.Email);
            }
        });
    }
});
```

## 解決してみた

こういう非同期処理のコールバックのネストを解消するデザインパターンとして古くから`Future/Promiseパターン`が利用されています。今回はこのデザインパターンと互換性のあるUniRxの`IObservable`に置き換えることで、このコールバックのネスト問題を解消してみました。

# NcmbAsObservable

というわけで、NCMB Unity PluginをUniRxで触れる用にするライブラリを作ってみました。

[NcmbAsObservable](https://github.com/TORISOUP/NcmbAsObservable)からダウンロードしてください。
_(いいライブラリ名が思いつかなかったので適当です)_

## 使い方

`using NcmbAsObservables;` をまず追加してください。それでいろいろ呼び出せるようになります。

### 例1: ログインしてデータ取得

`NCMBUser`にstaticで生えているAPIメソッドは`ObservableFromNcmbUser`から呼び出すことができます。

```csharp:ログインしてデータ取得
ObservableFromNcmbUser
    .LogInAsync("test_user_name", "hogehoge") //Login
    .SelectMany(u => u.FetchAsyncAsStream())  //Fetch
    .Subscribe(u => Debug.Log(u.Email), e => Debug.LogError(e));
```

このように`IObservable`化してしまえばコールバックのネストが消えてフラットにコードを書くことが出来るようになります。

### 例2:サインインしてユーザ情報を書き換える

NCMBが用意したオブジェクトに生えている`～Async`系のAPIは末尾を`AsyncAsStream`と呼び替えることで`IObservable`版を呼び出すことができるようになります。
(本当は `～AsyncAsObservable` にしたかったんだけど、長さ1のストリームをObservableと呼ぶのに抵抗があったためこんな変な名前になりました。他に良い命名があればプルリクお願いします。)

```csharp:サインインしてユーザ情報を書き換える

var user = new NCMBUser();

user.UserName = "test_user_name";
user.Password = "hogehoge";

//サインイン→成功したらデータ書き換え→再取得→結果表示

//Signup
user.SignUpAsyncAsStream()
.SelectMany(u =>
{
    //Change email and Age column when signed up
    u.Email = "test@test.com";
    u["Age"] = 20;
    return u.SaveAsyncAsStream(); //Save
})
.SelectMany(u => u.FetchAsyncAsStream()) //Fetch
.Subscribe(u =>
{
    Debug.Log(string.Format("{0}\t{1}\t{2}", u.UserName, u.Email, u["Age"]));
}, e =>
{
    Debug.LogError(" Error:" + e);
});
```

このように、`AsyncAsStream`に書き換えることで`IObservable`化することができます。
また、途中で発生したエラーは`OnError`を流れるのでこんな感じでエラーハンドリングもできます。

```csharp:例外が発生したポイントでログを出す
var user = new NCMBUser();

user.UserName = "test_user_name";
user.Password = "hogehoge";

//サインイン→成功したらデータ書き換え→再取得→結果表示

//Signup
user.SignUpAsyncAsStream()
.Catch((NCMBException e) =>
{
    Debug.LogError("Error on sign up:" + e);
    return Observable.Empty<NCMBUser>();
})
.SelectMany(u =>
{
    //Change email and Age column when signed up
    u.Email = "test@test.com";
    u["Age"] = 20;
    return u.SaveAsyncAsStream(); //Save
})
.Catch((NCMBException e) =>
{
    Debug.LogError("Error on save:" + e);
    return Observable.Empty<NCMBUser>();
})
.SelectMany(u => u.FetchAsyncAsStream()) //Fetch
.Catch((NCMBException e) =>
{
    Debug.LogError("Error on Fetch:" + e);
    return Observable.Empty<NCMBUser>();
})
.Subscribe(u =>
{
    Debug.Log(string.Format("{0}\t{1}\t{2}", u.UserName, u.Email, u["Age"]));
}, e =>
{
    Debug.LogError("Unknown Error:" + e);
});
```

##例3: NCMBQueryを使う

~~`NCMBQuery`はNCMBのデータストアに対してクエリを発行してオブジェクトを検索する時に使うオブジェクトです。
この`NCMBQuery`は対応する型をジェネリックで渡さないといけない関係で拡張メソッド化ができませんでした。
なので代わりに `NCMBQueryHelper<T>`を用意したのでこちらを経由してAPIを実行すれば`IObservable`化できるようになっています。~~

`NCMBQuery`も同様に拡張メソッド経由でObservableとして扱えるようになっています。

```csharp:NCMBQueryHelper
var query = new NCMBQuery<NCMBObject>("Score");
query.OrderByDescending("score");
query.Limit = 5;

query
    .FindAsyncAsStream()
    .Subscribe(resultList =>
    {
        foreach (var o in resultList)
        {
            Debug.Log(o);
        }
    }, error => Debug.LogError(error));
```

# 最後に

触れる頻度の高い`NCMBUser`を中心に`IObservable`化しています。
まだコールバックをとる非同期APIの`IObservable`化に漏れがあるかもしれませんので、見つけた時は報告いただけるかプルリクをくれるとありがたいです。