読者です 読者をやめる 読者になる 読者になる

ゲーム業界エンジニアの独り言

クライアント、サーバ、インフラなどなど興味あることなんでも紹介

C++ APIスタイル(データ駆動型API)

今、「C++のためのAPIデザイン」を読んでます。 API機能を表現するための方法(スタイル)についてまとめてみました。

C++のためのAPIデザイン

C++のためのAPIデザイン

APIスタイルについて・・・

この4つのスタイルのAPIを解説し、状況によって適切なスタイルの 選び方を提示します。

この記事では、以下ののAPIスタイルを解説します。

  • データ駆動型API

データ駆動型API

データ駆動型APIとは・・・

データ駆動型プログラムとは、異なる入力データを供給する事で、 実行するごとに異なる操作を行うものです。

例えば、ディスク上に保存された命令実行リストを格納したファイル名を、 受け取るだけのプログラムがこの例です。

この手法がAPI設計にインパクトをもたらすのは、 様々なメソッド呼び出しを、提供するオブジェクトの集合に依存するのではなく、 名前付き命令や、名前付き、引数のディクショナリを受け取るもっと一般的な、 ルーチンを提供できるからです。

これは、メッセージ伝達APIまたは、イベントベースAPIと呼ばれます。 以下の関数の呼び出し例では、この種のAPIと標準なC/C++の呼び出しとの違いを示します。

  • func(obj, a, b, c): フラットなCスタイルの関数
  • obj.func(a, b, c): オブジェクト指向C++の関数
  • send("func", a, b, c): パラメータ付きのデータ駆動型関数
  • send("func", dict(arg1 = a, arg2 = b, arg3 = c):名前付き引数のディクショナ付きデータ駆動型関数(疑似コード)

具体的な例として、データ駆動型モデルでスタックの例を再設計してみます。

class Stack
{
public:
    Stack();
    Result Command(const std::string &command, const ArgList &args);
};

このシンプルなクラスは複数な操作を行うのに使用されます。

stack = new Stack();
if (stack)
{
    stack->command("Push", ArgList().Add("value", 10));
    stack->command("Push", ArgList().Add("value", 3));
    Stack::Result result = stack->command("IsEmpty");
    while (!result->convertToBool())
    {
        stack->command("Pop");
        result = stack->command("IsEmpty");
    }
    delete stack;
} 

これは文字列データで指定した、複数入力をサポートする 単一メソッドCommandで個々のメソッドを置き換えています。

各種コマンドを格納したテキストファイルの内容をパースし、 実行するプログラムを順に記述することは簡単です。

# データ駆動型スタックAPIの入力ファイル
Push value:10
Push value:3
Pop
Pop

データ駆動型スタックAPIを使って、このデータを受け取るプログラムは、 各行のスペースで区切られた最初の文字列を取るだけです。 (便宜上、#はコメントとして無視する) その後、コマンドに続くスペースで区切られた文字列に対して、 ArgListの構造体を作成します。 それから、Stack::Command()を実行し、ファイルの残りの処理を実行します。

このプログラムは異なるテキストファイルを共有する事で、 大幅に異なるスタック動作を実行できます。 しかも、プログラムを再コンパイルする必要もありません。

データ駆動型のWebサービス

APIによってコマンドがサーバに送られ、オプションとして結果をクライアントに返す クライアント/サーバ・アプリケーションをはじめとしたステートレスな、 コミニケーションチャネルには、データ駆動型が特に適しています。

また、疎結合コンポーネント間でメッセージを伝達する場合にも有効です。

中でも、Webサービスはデータ駆動型APIで表現するのに最適です。 Webサービスはクエリをパラメータの集合と共に、URLを送信したり、 JSONまたはXMLなど、構造化形式のメッセージを送ったりしてアクセスします。

例えば、ソーシャルサイトのDiggは、ユーザーがDigg.comのWebサービスと やり取りできるAPIをサポートしています。 具体例として、ある記事の詳細を読みたいユーザーのために、拡張情報を 返すAPIを提供しています。

これは以下の形式で、HTTPのGETリクエストを送信することで呼び出します。

http://service.digg.com/1.0/endpoint?method=digg.getinfo&digg_id=id

これは先程、提示したデータ駆動型APIにもうまく当てはまります。 先の例では、このようなHTTPリクエストを以下のように呼び出す事ができます。

digg = DiggWebServie();
digg->Request("digg.getinfo", ArgList().Add("digg_id", id)); 

データ駆動型APIのメリット

データ駆動型APIのメリットを以下にあげます。

プログラムのビジネスロジックを編集可能なデータファイルに抽象化 つまり、実行ファイルを再コンパイルせずに、プログラムの動作を修正できます。 しかも、ユーザーがこのデータファイルを簡単に記述できるように、 別のデザインツールをサポートする事もできます。

将来的なAPIの変更に対して十分に対応できる コマンドの追加や、削除、変更を行っても、多くの場合、 パブリックAPIメソッドのシグネイチャに影響がありません。

例えば、データ駆動型のスタックAPIを例にとってみましょう。 このAPIの新バージョンでは、Topコマンドを追加し、Pushコマンドを拡張し、 複数の値を受け取って、各値を順番にスタックにプッシュするようにしたとしましょう。

stack = new Stack();
stack->Command("Push", ArgList().Add("value", 10)..Add("value", 3));
Stack::Result result = stack->Command("Top");
int top = result.ToInt();

この新機能を追加しても、ヘッダファイルの関数シグネイチャには まったく何も変化がありません。 サポートした文字列と、Commandリストに渡す引数を変更しただけです。 この性質のため、データ駆動型APIでは、後方互換性を保ちながら、 変更を行う事がずっと簡単になります。

テストをサポートしやすい これは個別のテストプログラムやルーチンを、大量に記述して行う代わりに、 自動でAPIテストを行うテクニックです。

具体的には、実行する一連のコマンドや、チェックすべきアサートを格納した、 ファイルを読み込むデータ駆動型プログラムを用意するだけです。 新規テスト作成にコンパイルの必要がないため、テスト開発の繰り返しが 早くできるし、C++開発スキルがあまりないQAエンジニアでも、テストを記述できます。

IsEmpty => True // スタックは空
Push value:10
Push value:3
IsEmpty => False // 2つの要素を持つので空ではない
Pop => 3
IsEmpty => False // 1つの要素を持つので空ではない
Pop => 10
IsEmpty => True // スタックは空
Pop => NULL // 空のスタックをPopするとエラーになる

このテストプログラムは、データファイルからスタックコマンドを読み取った、 以前のプログラムに非常によく似ています。 主な違いは、[=>]サポートを追加して、Stack::Commandの返り値の、 結果をチェック出来るようにしたことです。 たったこれだけの事で、あなたのAPIに対して、無数のデータ駆動型テストを 作成できる柔軟性のある、テストフレームワークに出来上がっています。

データ駆動型APIのデメリット

データ駆動型APIのデメリットを以下にあげます。

実行時のコストがかかる このAPIのシンプルさと安定性には実行時のコストが付いてまわります。 これはコマンド名の文字列を呼び出すために、適切な内部ルーチンを検索する オーバーヘッドが余計にかかるためです。

ユーザーがパブリックヘッダを見ただけでは機能が分からない ユーザーがパブリックヘッダを見るだけでは、インターフェイスによって、 どんな機能とセマンティックが提供されているのかを、把握する事はできません。 とはいえ、サポートされているコマンドと適切な引数のリストを明示した、 優れたAPIドキュメントを用意すれば、このデメリットを十分に補う事ができます。

インターフェイスコンパイル時チェックをしてもメリットがない パラメータの解析と、型チェックはAPI開発で行う事になります。 そこでコードをテストし、重要な動作を壊さないことを確認するのは、 開発者の型にかかるため、開発者の責任は大きくなります。