「Webサービスを設計するための単純明快な方法」の続き、あるいは補足です。
内容:
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要素)の対応を付けることは容易です。
ハイパーオブジェクトの基礎フォーマットとしてJSONやXMLを採用すれば(そして多少頑張れば)、人間の知的解釈を不要にできます。(X)HTMLを使う場合でも、アノテーションやコンベンションでルール化をすれば、「人間の知的解釈」なしでも最低限のHTML文書の生成は可能でしょう。最低限のHTML文書とは、クリーンHTM文書(「Webサービスを設計するための単純明快な方法」の冒頭を参照)のことです。
ハイパーオブジェクトを返すRPC
トリガーは結局、サーバー側処理の呼び出しを表現します。RPC(遠隔手続き呼び出し)の呼び出し側スタブだという解釈です。ただし、従来のRPCと違う点は、手続きがハイパーオブジェクトを返し、戻り値であるハイパーオブジェクトがクライアント側状態遷移を引き起こす点です。「アクションの戻り値=ハイパーオブジェクト=クライアント側状態」と考えます。これは、ブラウザによる素朴な閲覧行為とまったく同じモデルなので、「API体系は、人がブラウザで閲覧するためのサイトとまったく同じ構造にする」ことになります。
注意しないとRPCの弊害を再現させてしまいますが、僕は「抽象化された動作/行為」というダイナミックな概念(それがアクション)なしにサービスを設計するのはどうも困難だと感じるので、トリガー、ハイパーオブジェクト、アクションといった概念と用語は必要だと思うのですよ。
*1:[追記]「Webサービスの設計: ハイパーオブジェクトはワークフローやインターフェースも運ぶ」にメリットを書きました。 [/追記]
*2:現在のCatyスキーマでは、undefinedの代わりにnever?と書きます。予約語を増やしたくないという事情ですが、never?は分かりにくいかも。
*3:verbは特定の文字列に固定するか、使わないのがいいと思います。その指定には見慣れないスキーマ構文が出てくるので、とりあえずそのままにしておきます。
*4:HTTPSを使うかどうか、とかは今の議論とは別の問題です。