GOOS本の手法をWebアプリ開発に活かしたい

実践テスト駆動開発 テストに導かれてオブジェクト指向ソフトウェアを育てる』(GOOS本)に刺激を受けた。本書の手法をWebアプリ開発に活かしていきたい。

訳者の一人である和智さんの記事にあるとおり、「GOOS流TDDの核心は受け入れテストとユニットテストにより構成される二重のループ」である。二重のループ自体は2007年の記事「[動画で解説]和田卓人の“テスト駆動開発”講座:第8回 テスト駆動開発の「サイクル」――まず受け入れテストで土台を作る」でも紹介されており、目新しい手法ではない。本書の価値は、オークション入札アプリの開発を例に、二重のループを「実践」している点にある。どのように実践しているかは、本書を読んでのお楽しみ。

本書では、オークション入札アプリをSwingで構築しているが、Webアプリ開発にだって、二重のループを適用できるはずだ。原著者の一人であるSteve Freeman氏のGOOSメーリングリストへの投稿によると、少なくとも受け入れテストのレベルでは、GUIアプリでもWebアプリでもテストの書き方はさほど変わらないだろうとのことだ。実際、いわゆるPet Storeアプリを二重のループで書いたとの投稿もある。アプリのリポジトリをながめると、下記の受け入れテスト部分はたしかに、本書のオークション入札アプリのそれに似ている。

ちなみに僕は、Capybaraで受け入れテストを書くのが面白そうだと思っている。Webアプリのネタが浮かんだら、ぜひ書いてみたい。

CSRF対策のhiddenパラメータとHTTPキャッシュ

CSRF対策として、hiddenパラメータにセッションIDを出力する手法がある。この手法を採用するとき、HTTPレスポンスのキャッシュはどう制御すべきだろうか。

非共有キャッシュは活用したい

まず、一切キャッシュさせない方針がありうる(Cache-Control: no-store)。これで問題ないのであれば、もう何も考える必要はない。今回の記事では、そうではなく、非共有キャッシュを活用する方針を前提としたい(Cache-Control: private)。REST厨だからだ。

ここから先は、キャッシュの有効期限を明示的に指定できるかどうかによって話が変わってくる。

有効期限を明示的に指定できる場合

有効期限を明示的に指定できる場合は、以下のようなレスポンスヘッダを返すのがよいだろう。

Cache-Control: private,max-age=1800
Vary: Cookie

Cache-Controlで、非共有キャッシュへのキャッシュを許可するとともに、有効期限を明示的に指定する。

重要なのは「Vary: Cookie」だ。指定しないと、再ログインによってセッションIDが変わったり、ログアウトによってセッションIDが無効になったりしても、有効期限内であればキャッシュが使われてしまう。

有効期限を明示的に指定できない場合

有効期限を明示的に指定できない場合は、以下のようなレスポンスヘッダを返すのがよいだろう。

Cache-Control: private,must-revalidate
ETag: "hogehoge"
Last-Modified: Thu, 13 Sep 2012 01:23:45 GMT

有効期限を明示的に指定できないのは、常に新鮮なレスポンスを返したいからだ。よって「Cache-Control: must-revalidate」を指定し、キャッシュの常時検証をクライアントに指示するのがよいだろう。

検証条件として、上記の例ではETagとLast-Modifiedを返している。少なくとも一方は返し、If-None-MatchやIf-Modified-Sinceを用いた条件付きGETに対応する必要がある。

「Vary: Cookie」がないのは、セッションIDの変化に応じてETagやLast-Modifiedが変わるのを前提としているためだ。気になるなら返してもよいだろう。

JavaScriptでキャッシュをさらに有効に使う

さらに考えると、セッションIDが変わっただけでキャッシュが無効になるのは、もったいない気がしてくる。hiddenパラメータへのセッションIDのセットをクライアントサイドで処理するのはどうだろうか。文脈は違うが「Kazuho@Cybozu Labs: CSRF 対策 w. JavaScript」や「畑@サイボウズ・ラボ - CSRF対策 with JavaScript」で紹介されている手法である。

メリットとして、レスポンスにプライベートな情報が含まれない場合に、共有キャッシュの使用が可能になる(Cache-Control: public)。

また、セッションIDが変わっただけではキャッシュの検証が不要になる。キャッシュのヒット率が上がるし、サーバ側はリソースの本質的な変化だけを気にすればよいから実装が楽になる。良いことずくめではないだろうか。

ハイパーリンクは「状態の表現」をつなぐ

RESTful Webアーキテクチャにおいて、a要素やform要素に代表されるハイパーリンクがつなぐものは、リソースではなく「状態の表現」である。例として、POSTメソッドのレスポンスを考えると分かりやすい。ユーザがとくに知りたいのはPOSTの結果がどうなったかだ。サーバは、結果を表すリソースのURIをLocationヘッダで返すことも可能ではあるが、レスポンス自体に結果を含めてもよい(むしろ後者のほうがよく見かける)。このときレスポンスは「POSTの結果がどうなったか」をあらわす状態の表現であり、リソースそのものではない。

GETでも事情は変わらない。そもそも、リソースは転送(Transfer)されないのだ。転送されるのは状態の表現(Representational State)でしかない。

URI are identifiers of resources.  GET is a request for a
representation of the current state of the identified resource.
The server does not transfer the resource itself because that's
not what the client requested; the client does not want the
mechanism that implements the resource over all time -- only the
current state of the resource at that instant in time.  That's
what allows the client, and the transfer protocol, to be simple.

Re: resource and representation from Roy T. Fielding on 2002-07-08 (www-tag@w3.org from July 2002)

リンクが含まれるのも状態の表現だし、リンクを通じて得られるものもまた状態の表現なのである。

先日「ハイパーリンクは何をつないでもいい」を書いた時点では、このことが理解できていなかった。

「接続性」が誤解のもと

ハイパーリンクはリソースをつなぐべき、と僕が誤解してしまったのは、『RESTful Webサービス』で定義された「接続性」(Connectedness)が原因かもしれない。同書100ページに「リソースの接続性」とあるし、128ページには「リソースからリソースへの移動」とも書かれている。著者の意図はともかく、これらの記述から、リソース同士をつなぐのはハイパーリンク、というメンタルモデルが僕のうちにできてしまった可能性は大いにある。

Roy T. Fieldingは、接続性の定義に批判的だ。「アプリケーション状態エンジンとしてのハイパーメディア」を「接続性」と呼び替えられると、ハイパーメディアの駆動力としての役割が不明確になってしまう、というのだ(On software architecture - Untangled)。「ROAはリソースを重視しすぎだ」とも言っている。その弊害のひとつが、僕の誤解に現れてしまったのかもしれない。

プロフィール
知識欲と謎解き欲が旺盛なWebエンジニア。AWS認定ソリューションアーキテクト - アソシエイト。JAPAN MENSA会員
記事検索