Webサービスの設計: ハイパーオブジェクトとトリガー

「Webサービスを設計するための単純明快な方法」の続き、あるいは補足です。

内容:

  1. Web APIもWebサイトも同じ
  2. トリガーとは
  3. トリガーの構造
  4. トリガーについてもっと
  5. ハイパーオブジェクトを返すRPC

Web APIもWebサイトも同じ

僕の方針は、プログラムが利用するWeb APIであっても、次の原則で設計することです。

  • API体系は、人がブラウザで閲覧するためのサイトとまったく同じ構造にする。

人間用のサイトを一切作らないときでも同じ原則を適用します。転送オブジェクト(レスポンスのエンティティボディに入るデータ)の形式が(X)HTMLでないときでも同じ原則に従います。

「人間+ブラウザ」用の転送オブジェクトの形式(フォーマット)といえば、もちろんHTMLです。HTMLの最も重要な特徴はハイパーリンクです。ハイパーリンクがWebを形作っているのです。ですから、HTML以外のフォーマットを使うときでもハイパーリンク機能は絶対に使うべきです! 個々のフォーマットの詳細は捨象して、ともかくもハイパーリンク機能を持つようなデータ(Webの転送オブジェクト)をハイパーオブジェクトと呼ぶことにします。ハイパーオブジェクトに含まれるハイパーリンク(のアンカー)をトリガーとも呼びます(こう呼ぶ理由は後述)。

トリガーとは

僕は「Webとはハイパーリンクなり」と考えているので、Web APIでもなんでもハイパーリンクを使ってないなら「Webっぽい」とは思いません。RPC(遠隔手続き呼び出し)的な要素を取り入れても、ハイパーリンクを活用しているならWebっぽいでしょう。「っぽい」とか「らしい」は単なる趣味嗜好の問題ではなくて、ハイパーリンクの活用は大きなメリットがあります(そのメリットの説明は今日はしませんが*1)。

さて、HTMLフォーマットとHTML文書は典型的かつ最重要なハイパーオブジェクトです。a要素、form要素、link要素がそのトリガーです。場合により、img要素、script要素もトリガーと考えることがあります。トリガーはURL(href属性、src属性)を持っていて、そのURLが指し示すリソースや処理をサーバ側に要求するリモコン・ボタンとなります。

単純なアンカー <a id="ChimairaSite" href="http://www.chimaira.org/">キマイラ サイト</a> はトリガーですが、{"id" : "ChimairaSite", "href" : "http://www.chimaira.org/", "text" : "キマイラ サイト"} というJSONオブジェクトに同じハイパーリンク機能を持たせれば、これはJSONにおけるトリガーになります。そして、このようなトリガーを含むJSONデータはハイパーオブジェクトとなるのです。(「JSONだってハイパーメディア -- JSONハイパースキーマ仕様をなんとかしたい」も参照。)

あえてトリガーと呼ぶ理由を述べておきます; 本来のハイパーリンクは、リソース間の関係を表すものです。3つ以上のリソースが関与する関係でもかまわないし、方向性を持たなくてもかまいません。しかし現実には、「2つのリソースの間の方向を持つ関係」をインライン方式(どちらかのリソースにリンク記述を埋め込む方式)で表すリンクしか使われていません。それと、form要素も仲間に入れると「二者間の関係」という解釈は苦しくなります。

そこで、手続き呼び出し的な解釈を採用して、サーバー側のなんらかの処理(アクションと呼びます)を呼び出すデータ構造とメカニズムは総じてトリガーと呼ぶことにします。「手続き呼び出し」と聞くだけで顔をしかめる人もいるでしょうが、便利で分かりやすいなら、特定の宗派や党派に与する必要はありません。それに、リソース(の表現)を転送するだけのアクションを呼び出すなら、それはリソース指向と整合します。

トリガーの構造

HTMLのa要素、form要素、link要素において、トリガーとして必要な情報は属性に指定されます。トリガーに関係する属性を列挙してみます。記述形式としてはCatyスキーマ言語を使います。(Catyスキーマ言語の構文は見れば分かるものです。)


type uri = string(format="uri");
type mediaType = string(format="media-type");
type httpMethod = ("GET" | "PUT" | "POST" | "DELETE" | "HEAD");

type Trigger<InputType> = {
// 個々のトリガーを識別する属性
"id" : string?,
"name" : string?,
"class" : string?,

// ハイパーリンクの記述
"href" : uri,
"rel" : string?,
"rev" : string?,
"type" : mediaType?,
"method" : httpMethod?,

// 手続き呼び出し的なデータ項目
"verb" : string?,
"input" : InputType,

// その他いろいろ
* : any?
};

ここで、InputTypeは型変数で、実際に使うときは何らかの型に具体化されます。変数としての型InputTypeは、フォームの入力に要求される型だと思ってください。ですから、入力を伴わないトリガーのときは無視できます(InputTypeの値をnullとかundefined*2にする)。

個々の応用で使うトリガー型は、上に定義した最も一般的と思える型(総称型)のサブタイプになるように定義します。例えば、単純なアンカーは次のように定義できます。


type SimpleAnchor = {
"id" : string?,
"href" : uri,
"text" : string
};

このSimpleAnchor型のインスタンスのひとつが {"id" : "ChimairaSite", "href" : "http://www.chimaira.org/", "text" : "キマイラ サイト"} です。

トリガーについてもっと

次の型もトリガー型になります。


type PostForm<InputType> = {
"href" : uri,
"method" : "POST", // POSTに固定
"verb" : string?,
"input" : InputType, // 色々な型を許す

* : any?
};

PostForm型には型変数InputTypeが含まれるので、これを具体化してみます。


type UserInfo = {
"userId" : string(minLength=3, maxLength=12),
"password" : string(minLength=6, maxLength=16)
};

type UserLoginForm = PostForm<UesrInfo>;

念のため、型変数が具体化された後の形を書いてみると*3


type UserLoginForm = {
"href" : uri,
"method" : "POST",
"verb" : string?,
"input" : {
"userId" : string(minLength=3, maxLength=12),
"password" : string(minLength=6, maxLength=16)
},

* : any?
};

このデータ型のインスタンスとしては次があります。


{
"href" : "http://example.jp/user/login.cgi",
"method" : "POST",
"input" : {
"userId" : "m-hiyama",
"password" : "xxxxyyyy"
}
};

このなかで、inputプロパティの値である {"userId" : "m-hiyama", "password" : "xxxxyyyy"} はサーバー側に送られて、アクションの入力になります。対応するHTMLフォームは次のようになるでしょう(実用的にはひど過ぎる表示ですが)*4

<form
  action ="http://example.jp/user/login.cgi"
  method = "POST"
>
  <input type="text" name="userId" /> <br />
  <input type="password" name="password" /> <br />

  <input type="submit" />
</form>

トリガーのデータ型定義やインスタンスから、HTMLアンカーやHTMLフォームを自動生成するには情報が不足ですが、人間の知的解釈が介在すれば、トリガーとHTML要素(a要素、form要素、link要素)の対応を付けることは容易です。

ハイパーオブジェクトの基礎フォーマットとしてJSONXMLを採用すれば(そして多少頑張れば)、人間の知的解釈を不要にできます。(X)HTMLを使う場合でも、アノテーションやコンベンションでルール化をすれば、「人間の知的解釈」なしでも最低限のHTML文書の生成は可能でしょう。最低限のHTML文書とは、クリーンHTM文書(「Webサービスを設計するための単純明快な方法」の冒頭を参照)のことです。

ハイパーオブジェクトを返すRPC

トリガーは結局、サーバー側処理の呼び出しを表現します。RPC(遠隔手続き呼び出し)の呼び出し側スタブだという解釈です。ただし、従来のRPCと違う点は、手続きがハイパーオブジェクトを返し、戻り値であるハイパーオブジェクトがクライアント側状態遷移を引き起こす点です。「アクションの戻り値=ハイパーオブジェクト=クライアント側状態」と考えます。これは、ブラウザによる素朴な閲覧行為とまったく同じモデルなので、「API体系は、人がブラウザで閲覧するためのサイトとまったく同じ構造にする」ことになります。

注意しないとRPCの弊害を再現させてしまいますが、僕は「抽象化された動作/行為」というダイナミックな概念(それがアクション)なしにサービスを設計するのはどうも困難だと感じるので、トリガー、ハイパーオブジェクト、アクションといった概念と用語は必要だと思うのですよ。

*1:[追記]「Webサービスの設計: ハイパーオブジェクトはワークフローやインターフェースも運ぶ」にメリットを書きました。 [/追記]

*2:現在のCatyスキーマでは、undefinedの代わりにnever?と書きます。予約語を増やしたくないという事情ですが、never?は分かりにくいかも。

*3:verbは特定の文字列に固定するか、使わないのがいいと思います。その指定には見慣れないスキーマ構文が出てくるので、とりあえずそのままにしておきます。

*4:HTTPSを使うかどうか、とかは今の議論とは別の問題です。