Notes C API & C++11+ & ReactiveX & Qt #8 ~RxでNotes C API関数をラップ

お題「Notes C API関数をReactiveXでラップする」

今回はこのお題について考えます。なお、今回のコードは、前回ご紹介した以下のサイトを使います。

github.com

Gitを使ってクローンして、v0.0.2をチェックアウトします。

git clone https://github.com/Chiburu/notespp.git
cd notespp
git checkout v0.0.2

そもそもReactiveXとは?

という方は、ぜひとも「ReactiveXとは?」でググってみてください。つい3~4ヶ月前まで「ReactiveX」の「り」の字も知らなかった私を含め、デザインパターン「Observer」や「Iterator」、関数型プログラミングなどに触れたことがない人にとっては、「ググってもわからない」というのが正直なところでしょう。わからなかったことがなんとなくわかるようになったことは、この連載の#1でも触れています。あの記事から2ヶ月が経ちましたが、わからないことはまだまだ多い!・・・ですが、もうRxなしは考えられない状況です。先日はLotusScriptでもRxっぽいライブラリを作ってしまいました。この連載でも「なんとなく使ってみると、こんな風になりました」という感じで紹介できたらと思っています。

目標コード

今回の目標は、以下のコードを・・・

  WORD serverVersion = 0;
  DWORD clientToServer_ms, serverToClient_ms;
  STATUS status = NSFGetServerLatency(
        const_cast<char*>(pServer),
        0,
        &clientToServer_ms, &serverToClient_ms,
        &serverVersion
        );
  out << QObject::tr("NSFGetServerLatency status")
      << ": "
      << ERR(status)
      << endl;
  if (ERR(status) != NOERROR) { return 1; }
  out << QObject::tr("Build version of '%1'").arg(pServer)
      << ": "
      << serverVersion
      << endl
      << QObject::tr("Latency time for client to server")
      << ": "
      << QString("%1 ms").arg(clientToServer_ms)
      << endl
      << QObject::tr("Latency time for server to client")
      << ": "
      << QString("%1 ms").arg(serverToClient_ms)
      << endl;

次のようにすることです。

  nx::GetServerLatency()(pServer, 0).subscribe(
        [&pServer](nx::GetServerLatency::ReturnValues values) {
    QTextStream out(stdout, QIODevice::WriteOnly);
    out << QObject::tr("Build version of '%1'").arg(pServer)
        << ": "
        << values.version_
        << endl
        << QObject::tr("Latency time for client to server")
        << ": "
        << QString("%1 ms").arg(values.clientToServer_)
        << endl
        << QObject::tr("Latency time for server to client")
        << ": "
        << QString("%1 ms").arg(values.serverToClient_)
        << endl;
  }
  , [](std::exception_ptr ep) {
    try {std::rethrow_exception(ep);}
    catch (nx::Status status) {
      QTextStream out(stderr, QIODevice::WriteOnly);
      out << QObject::tr("NSFGetServerLatency status")
          << ": "
          << status.error()
          << endl;
    }
  });

もう少し短くしてみますね。

STATUS status = NSFGetServerLatency(const_cast<char*>(pServer), 0, &clientToServer_ms, &serverToClient_ms, &serverVersion);
if (ERR(status) != NOERROR) { /*異常処理*/ } else { /*正常処理*/ }

こんな風に書いているのを、

GetServerLatency()(pServer, 0).subscribe(
  [](ReturnValues values) { /* 正常処理 */ },
  [](std::exception_ptr ep) { /* 異常処理 */ });

こんな風にリファクタリングしてみようということです。

あらためてNSFGetServerLatency

この関数の仕事は、目的のサーバと通信するときの待ち時間の計測と、サーバのバージョンの取得です。

入力

  1. サーバ名
  2. タイムアウト時間
  3. クライアントからサーバへの待ち時間を取得するなら変数へのポインタ、取得しないならヌルポインタ
  4. サーバからクライアントへの待ち時間を取得するなら変数へのポインタ、取得しないならヌルポインタ
  5. サーババージョンを取得するなら変数へのポインタ、取得しないならヌルポインタ

出力

  1. ステータス値、正常に取得できればNOERROR(0)、異常があれば非ゼロ値
  2. クライアントからサーバへの待ち時間(取得のための変数へのポインタを指定した場合)
  3. サーバからクライアントへの待ち時間(取得のための変数へのポインタを指定した場合)
  4. サーババージョン(取得のための変数へのポインタを指定した場合)

面倒なことは隠そう

この関数の面倒なところは、何と言ってもヌルポインタによる「取得したくない」の意思表示です。3つの値を取得するかしないかは、デフォルト「取得する」でいいのではないかと思うわけです。取得したくないときは、そのデフォルトを変更できるようにしてあげれば、関数の機能としても型落ちすることがありません。

また、戻り値について、3つの値のための変数をひとつひとつ用意するのは面倒です。構造体を用意して、3つの値を一括で返してあげれば、受け取りも楽ですね。

というわけで、こんなクラスを考えてみました。

// notespp/notespp/server.h

/**
 * @brief サーバへの待ち時間、サーバからの待ち時間、サーバのバージョンを取得する関数オブジェクト。
 * @class GetServerLatency
 */
class NOTESPPSHARED_EXPORT GetServerLatency
{
public:
  /**
   * @brief 内部管理用値クラス
   * @class InnerValue
   * @tparam T データ型
   */
  template <typename T>
  class InnerValue
  {
  public:
    InnerValue(const T &value, bool enabled)
      : value_(value)
      , enabled_(enabled)
    {}

    T *pValue() { return (enabled_) ? &value_ : nullptr; }
    T value() const { return value_; }

  private:
    T value_;
    bool enabled_;
  };

  /**
   * @brief 出力用データ型
   * @struct ReturnValues
   */
  struct ReturnValues
  {
    WORD version_;
    DWORD clientToServer_;
    DWORD serverToClient_;
  };

  /**
   * @brief コンストラクタ
   * @param enableVersion バージョン取得を有効にする
   * @param enableClientToServer クライアントtoサーバを有効にする
   * @param enableServerToClient サーバtoクライアントを有効にする
   */
  GetServerLatency(
      bool enableVersion = true,
      bool enableClientToServer = true,
      bool enableServerToClient = true
      );

  /**
   * @brief 関数を実行する。
   * @param serverName サーバ名
   * @param timeout タイムアウト時間(0ならデフォルト)
   * @return ReturnValuesを流すObservable
   */
  rx::observable<ReturnValues> operator ()(
      const QByteArray &serverName,
      DWORD timeout = 0
      );

private:
  InnerValue<WORD> version_;
  InnerValue<DWORD> clientToServer_;
  InnerValue<DWORD> serverToClient_;
};

このクラスは、俗に「関数オブジェクト」というタイプになります。簡単な例を示します。

class Plus {
public:
  Plus(int a):a_(a){}
  int operator()(int b){ return a_ + b; }
private:
  int a_;
};

int main() {
  Plus plus_one(1);
  printf("1 + 2 = %d\n", plus_one(2)); // => 1 + 2 = 3
  printf("3 + 4 = %d\n", Plus(3)(4)); // => 3 + 4 = 7
}

main関数の1行目のplus_onePlusクラスの変数ですが、2行目では、あたかも変数を関数のようにして使っています。実体は「かっこ演算子」をオーバーロードしたインスタンスメソッドなワケですが、このように関数のごとく扱えるクラスのことを「関数オブジェクト」と呼びます。変数の宣言も省略すると、3行目のようなPlus(3)(4)と書くこともできます。

GetServerLatencyクラスのコンストラクタは、次のようになっています。

GetServerLatency(
    bool enableVersion = true,
    bool enableClientToServer = true,
    bool enableServerToClient = true
    );

どれを取得して、どれを取得しないということを、コンストラクタで決めています。ただし、すべての値にデフォルト値trueが設定されているので、変更しなくてよいのであれば、すべての入力値を省略することもできます。

実際の呼び出しは、かっこ演算子が担当しますが、「タイムアウト時間」もデフォルト値を指定できるので、これも既定値と考えると、次のようなメソッドになります。

rx::observable<ReturnValues> operator ()(
    const QByteArray &serverName,
    DWORD timeout = 0
    );

すべての既定値を活かして書くと、次のように書くこともできます。

GetServerLatency()("YourServer");

とてもコードがスッキリしました。ところで、この関数オブジェクトはrx::observable<ReturnValus>を返します。rx::observableを返すには、どんな書き方をすればいいのか、また、受け取り側はどんな風に受け取ればいいのか、次回説明します。