qwikWeb の仕組み 【第 1 回】 コンテンツ・フレームワークとしての qwikWeb

書いた人:えと こういちろう

はじめに

qwikWeb は、メーリングリストサーバと Wiki サーバが統合されたグループ・コミュ ニケーション・システムである。メーリングリストと Wiki の 長所を合わせたようなシステムとなっている。

本稿では、メーリングリストサーバと Wiki サーバの統合という特徴から一旦外れ、 qwikWeb の Wiki サーバ部分に着目し、実装の詳細について解説する。 (ここでは簡単のため、「qwikWeb の Wiki サーバ部分」のことを、単に「 qwikWeb 」 と呼ぶことにする。)

qwikWeb は、高機能な Wiki サーバであると同時に、様々な機能拡張を容易に 行えるように柔軟な構造を備えている。つまり、ある意味コンテンツ・フレー ムワークとして使えるような構造となっている。本稿では、qwikWeb が持つ基 本的構造について解説する。

本稿では特に、フレームワークとして見た場合の基本的特徴について扱う。実 際に qwikWeb を拡張する方法などについては、本稿では簡単に触れ、詳細に ついては次号以降で扱う予定である。

きっかけ

RubyConf2005 にて、まつもとさんとお話しする機会があり、qwikWeb はどのよ うな機能があるか、どのような構造になっているかを解説させていただいたと ころ、とても面白がってくれたと同時に、「あまりにマーケティング不足じゃ ないの?」という御意見をいただいた。たしかにいままで、技術的な側面につ いて宣伝したことはほとんど無かったので、おっしゃる通りである。

もう一つのきっかけは、Rails である。Rails はとても使いやすい Web アプリケー ション・フレームワークである。Rails を少し使ってみて、軽いデジャブを感じ た。私が普段 qwikWeb を開発している時の感覚にとてもよく似た面があ ると感じた。実は、Rails の作者である David Heinemeier Hansson は、Rails の 前に Instiki という Wiki サーバを作っていた。Instiki も WEBrick ベースの Wiki サーバであり、その時から様々な面で似た特徴を持っていると感じてい た。Instiki や Rails と qwikWeb はいくつか似た面を持っているが、その逆に qwikWeb にしか無いような特徴もあることを発見した。そこで、ここでは qwikWeb が持つ特徴的な面を中心に解説する。

現状を愛する

この文章は、「現状を愛する」という視点で書いている。自分自身が設計した システムを説明するというのはやっかいなことである。説明のための文章をま とめるということは、つまり自分の設計を見直すということであり、そうする とどうしても「ここはこうしておけばよかった」とか「ここはこう直したい」 といった気持ちが生まれる。しかし「ここはこう直してから文章を書こう」な どと考えていると、いつまでたっても文章は書けない。

この文章では、あえて改善を要する点には目をつぶり、まずは現状でできてい るシステムがどのように動いているかを解説する。その過程で浮びあがってき た改善を要する点はその後に直すことにしよう。また「ここはこう直した方が いい」とか「このやり方の方がいいのではないか」などといった意見がありま したら、ぜひ教えてください。今後の参考にさせていただきます。

qwikWeb の概略

qwikWeb における特徴的な開発手法

qwikWeb は、Wiki サーバを中心とした実用的なグループ・コミュニケーショ ン・システムであると同時に、自分が様々に考えている開発手法の実験の場と いう側面も持っている。私が思いついた新しい実験的な開発手法を、実践の場、 実用されている現場に投入してみて、どのように開発効率が変化するの かを実験している。つまり、自分自身を実験台として、日々実験を続けている わけだ。普通であれば取り入れないような面倒臭い仕様でも、その仕様を取り 入れることによって開発効率が大きく変化する可能性があると感じれば、積極 的にその方法を取り入れている。ここでは、そのような実験を経て、実際に良 いと感じた複数の知見について解説する。

まず一つの知見は、コードにおける HTMLの扱い方である。一般にプログラ ムから HTML を出力する際には、HTML を文字列として扱う。しかし HTML は本来は 構造を持った情報であり、文字列として扱った場合には、正しい構造を保持し 続けることを保証するのは難しい。終了タグを忘れてしまったり、順序を間違 えてしまったりすることはよくある。それだけでなく、うっかりサニタイズし忘 れてしまった場合には、セキュリティホールに直結してしまう。qwikWeb では 常に__「HTML を配列の集合として扱う」__ことによって、この問題を回避している。 このような工夫によって、サニタイズし忘れるということは原理的になくなった。また それ以外の面においても、様々な利点があるため、それらの 利点について解説する。

またもう一つの知見は、__「テスト重要」__ということ。これは誰もがみな言っ ていることであるが、実際にテスト重要を貫いている話はあまり聞かない。や はりテストケースを書くのが面倒というのがその理由ではないか。特に Web ア プリケーションを書く場合には、どうテストしていいのかのノウハウがまだあ まり蓄積されていない。qwikWeb では、Web アプリケーションとしての動作テス トのテストケースを、できるだけ簡単に書けるように工夫しており、それによっ て実際に qwikWeb が持つほとんど全ての機能についてテストケースを書いてい る。これにより、自信を持ってコードを改変することができるようになった。 どのような工夫によって、テストを書きやすくしているのかを後ほど説明する。

開発版のインストール方法

qwikWeb の構造について解説する前に、まずは qwikWeb のインストール方法 について説明する。実際にインストールをして、コードを見て、改変しながら の方が説明を理解しやすくなるだろう。

qwikWebには様々な拡張機能が存在するが、それらの拡張機能を全て使いたい 場合には、いくつかの拡張ライブラリをインストールする必要がある。実際に は、拡張ライブラリをインストールしなくても、標準的な Ruby だけで、基本 的な機能は全て使えるようになっているため、まずはインストールして使って みて、拡張機能を使いたくなってから拡張ライブラリをインストールするといっ た使い方もできる。

Debianの場合には、

% sudo apt-get install libopenssl-ruby libgd-ruby1.8 imagemagick

とする。

それ以外のプラットフォームの場合は、

をそれぞれインストールしてほしい。

~/qwik 以下に qwikWeb を install すると仮定する。 まず、CVS より、開発の最新版を取得する。

% cd
% cvs -z8 -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/qwik co qwik

このようにして、最新版を取得する。次に、qwikWeb を起動する。

% cd qwik
% ruby bin/qwikweb-server -d -c etc/config-debug.txt
[2005-12-13 01:10:53] INFO  WEBrick 1.3.1
[2005-12-13 01:10:53] INFO  ruby 1.8.3 (2005-09-21) [i386-cygwin]
[2005-12-13 01:10:53] INFO  Qwik::Server#start: pid=3280 port=9190

このように、CVS で取得したディレクトリに移動して、デバッグモー ドで qwikWeb を起動する。

Unix で、make コマンドが使える場合は、

% cd qwik
% make

として、単に make をうつだけでもよい。

qwikWeb が起動すると、WEBrick の起動メッセージが表示される。 http://127.0.0.1:9190/ にアクセスしてみると、qwikWeb の入口となるペー ジが表示されるはずだ。

もしこのようにしても起動しない場合は、意図した動作ではないため、 メーリングリストにてバグ報告していただけるとありがたい。 メーリングリストの参加方法は、下記の URL を参照してください。

このように、Wiki サーバはポート 9190 で立ち上がる。しかし、 ポート指定を含む URL は見た目が悪いため、 http://www.example.com/wiki/ といっ た URL にマッピングしたいと思うことが多いだろう。一般に、ポート 80 では Apache などの Web サーバがすでに走っているため、Apache の設定を変更して、ポー ト 80 からポート 9190 にリダイレクトする必要がある。このような Apache の設定 方法はここでは扱わない。qwik.jp のページを参照してほしい。

また、qwikWeb は Wiki サーバとメーリングリストサーバが統合されているとい う特徴を持つが、ここではメーリングリストサーバの設置方法については扱わ ない。メーリングリストサーバの立ち上げは、DNS の変更、メールサーバの 設定変更などが必要であり、Wiki サーバの立ち上げに比べると難易度が高い。 実際にメーリングリストサーバとして運用する場合には、いろいろとひっかか る点があるかもしれないので、その場合はメーリングリストで質問してください。

こういった実際の運用に必要な知見はまだあまりまとめられていない。 今後まとめていく予定である。

qwikWeb の簡単な使い方

qwikWeb が localhost で立ち上がったところで、簡単な使い方を見ておこう。

にアクセスすると、入口となる FrontPage が表示されるはずだ。

この段階ではまだ「編集」というリンクは表示されていない。qwikWeb では、 かならず一旦ログインしてから編集するようにしている。右上に「 Login 」 というリンクが見えるが、そこをクリックしてログインページに行く。

ユーザ ID
guest@qwik
パスワード
80245996

というユーザ ID とパスワードを入力する。

入力するとまた FrontPage に戻るが、今度は右上に「編集」というリンクが見 えるはずだ。そのリンクを辿ると編集画面に移行する。そこでなんらかのテキ ストを入力すると、ページの内容が変更される。

他の qwikWeb の使い方や、様々なプラグインの説明は、編集画面の右上に 「qwikWeb の使い方」というリンクがあるので、それを辿ると使い方の一覧が 表示される。

ページ IDとページタイトル

通常の Wiki では、ページはページ名によって指示される。カギカッコ二つに囲 まれたテキストがページ名として扱われ、その URL 自体にページ名が埋め込ま れる。アルファベットだけのページ名であれば問題はないが、ページ名に日本 語が含まれる場合は、URL が長くなり、可読性が低くなるなどといった問題が 生じる。qwikWeb ではこの問題を、ページ ID とページタイトルという二つの概 念に分けて考えることによって回避している。実際にページを作って例示して みる。

例えば FrontPage を編集をして下記のような文章を埋め込んでみる。

これは[[テストページ]]です。

FrontPage を表示すると、「テストページ」というテキストの右に新規作成用 のアイコンが表示される。そのアイコンをクリックすると確認画面がでて、 「新規作成」ボタンを押すとページが作成される。なにか適当な文章を入力し て「保存」する。

そして、FrontPage に戻ってみると、「テストページ」という文字列はそのペー ジへのリンクになっているはずだ。そのページへのリンクは具体的には、

という URL を指しているはずだ。この URL の「 1.html 」の「 1 」の部分が、ペー ジ ID を示している。そしてそのページを表示したときのタイトル「テストペー ジ」がページタイトルである。

ページへのリンクは、ページタイトルを使っても、ページ ID を使ってもできる。 一旦 FrontPage に戻り、下記の文章を書いてみる。

これも[[1]]です。

この文を記入し、保存する。そうすると、画面には、

これもテストページです。

と表示されるだろう。このように、ページタイトルを使っても、ページ IDを使っ てもリンクを作ることができる。また、ページ ID を使ってリンクした場合には、 ページへのアンカーテキストは、そのページ ID ではなくページタイトルとなる。

このページタイトルは、ページテキストの一行目で指定される。 さきほどのテストページに戻り「編 集」ページに行く。ページの一行目は、

* テストページ

という見出しになっているだろう。qwikWeb ではこのように、ページの一行目 が見出しになっている場合、それをページタイトルとして扱うようになってい る。つまり、この一行目の見出しを編集すると、ページタイトルが変更される。 試しに少しだけページタイトルを変えてみよう。

* テスト用ページ

という風に変更して保存して、そしてもう一度 FrontPage に戻ってみる。今度 は、一行目は「テストページ」のままだが、二行目は「テスト用ページ」に更 新されている。このように、ページ ID を使ってリンクした場合は、そのリンク 先のページタイトルの変化が自動的に反映される。

この場合、一行目は本来はリンクが外れるはずなのだが、ページタイトルとペー ジ ID との対応関係をメモリ上にキャッシュしているため、まだリンクされたま まの状態となる。qwikWeb サーバを再起動すると、ページへのリンクが外れ右 側に新規作成アイコンが表示される。

わびさび方式

__「配列によって HTML を表す方法」__とは一体何だろうか。 まずは、現在使われている一般的な手法の解説からはじめよう。

HTML を文字列として扱う際の問題

HTML をプログラム中で扱う際は、文字列として扱うことが多い。例えばあるテ キストを strong タグで囲って強調するプラグインを考えてみよう。一般にはこ のようなコードになるだろう。

   def strong(text)
     return "<strong>#{text}</strong>"
   end

ここでは入力として与えられたテキストの回りを strong タグで囲って、 結果となる HTML を返している。 しかしこのコードにはセキュリティホールがある。ユーザから受け取った文字列を そのまま HTML として出力してしまっているため、 タグとなる文字列もそのまま出力されてしまう。 そのため、XSS 脆弱性が存在する。 次のように、入力されたテキストをサニタイズしてやる必要がある。

   def strong(text)
     return "<strong>#{text.sanitize}</strong>"
   end

つまり、 HTML を文字列として生成する個々のコードでは、プログラマは 入力として与えられる文字列を全てサニタイズする必要がある。 ある一つの閉じたシステムであれば、全てサニタイズしていることを 保証することはできるかもしれない。しかし、様々な人がプラグインを書き、それを 追加できるようなシステムの場合、それぞれのプラグイン全てが 正しくサニタイズしていることを保証することは大変難しい。

このようなセキュリティ上の問題に対しては、 「個々の開発者が気をつける」というアプローチではダメである。 そのような手法では、原理的にはセキュリティは確保できない。これは、そ もそもそのような記述をすることができないようにシステムを設計するべきなのである。

またもう一つには、終了タグを書く必要があるという問題もある。 開始タグも終了タグも両方書くということは、DRY 原則に反している。 このような 単純な事例では問題にならないが、複雑な HTML を出力する場合、正しく終了タ グを書いているかどうかを保証することは難しい。

HTML を配列として扱う

そこで、そのような問題を原理的に解決するべく考えだされた手法が、 __HTML を配列の集合として扱う手法__である。

この方式がどのようなものかは、具体例を見てみれば一目瞭然だろう。

require 'qwik/wabisabi-format-xml'

html = [:html,
  [:head,
    [:title, 'hello']],
  [:body,
    [:h1, 'hello, world!'],
    [:p, 'This is a ',
      [:a, {:href=>'hello.html'}, 'hello, world'],
      ' example.']]]

puts html.format_xml

ここではまず HTML を配列の集合として記述し、それを html にいれている。そし てそれに対して format_xml というメソッドを実行し、結果を puts で出力している。 1

HTML の要素におけるタグ名をシンボル、その中のテキストを普通の文字列、 そしてアトリビュートをハッシュで表している。要素の階層構造は、配列の入れ 子として扱う。このように、Ruby の基本的なオブジェクトの集合 だけで HTML の構造を示しており、 大掛かりな仕組みを導入することなく、HTML の構造を扱うことに成功している。 高林哲氏は、この方式を__「わびさび方式」__と名付けている。

qwikWeb の各々の処理では、常に HTML をこのわびさび方式で扱っ ている。最後にクライアントに HTML を返す際に、format_xml で まとめて文字列に変換して出 力している。 要素の中のテキストは format_xml で HTML に変換する過程で、自動的にサニタイ ズされる。そのため、サニタイズし忘れるということは原理的に無くなる。

実際には XSS 脆弱性の問題はもっとややこしい。 タグを埋め込めないようにすることは基本形であるが、 それ以外の場所にもスクリプトを埋め込むことはできる。 例えばアトリビュートや CSS の中にもスクリプトを埋め込むことはできる。 そのため、タグを書けなくするだけで XSS 脆弱性が無くなるわけではなく、 それ以外の要素についても注意が必要である。 しかし、もっとも基本形であるタグを埋め込めないようにすることを 自動化できるため、大きなメリットがある。

HTML の変形操作

わびさび方式の利点は、第一にはセキュリティである。しかし、このように HTML を常に配列の集合として扱うことによって、他にも利点が生じる。そ れは、HTML を配列の集合として扱っているために、配列の変形操作 によって、HTML を容易に変形できるということだ。これはつまり、 HTML を受け取り、HTML を返すというコードを書きやすくなったということだ。

例えば、下記のような HTML を考えてみる。

<a href="http://www.example.net/">
<a href="example.html">

このような HTML を受け取り、下記のような HTML に変形したいとする。

<a class="external" href="http://www.example.net/">
<a class="internal" href="example.html">

従来の手法では、HTML は文字列であるので、正規表現によるパターンマッチング で判定し、外部向けのリンクであれば class=”external” を追加し、内部リ ンクであれば class=”internal” を追加する。それは文字列の変形として実装する ことになる。

しかし、HTML の記法にも様々な種類がある。例えば「 <a 」の後に改行が来ていた らどうだろう。また、もしすでに class が指定されていたらどうだろう。 そのような場合にも対応可能な正規表現を書くことは不可能で はないが、とても面倒である。

わびさび方式の場合は配列やハッシュなどの基本的な要素の集合として構造化 しているため、あたえられた配列を変形して返すという操作を書くことは非常に容易 である。

従来はデータを HTML として文字列化した後はもう変形させられなかった ため、HTML を生成する 段階で全ての情報を用意し、一度に埋め込むという手法をとっていたが、わび さび方式を用いると、一度配列にしてから、その後で要素を変形させるという 手法が可能となる。つまり、HTML を入力データとして使うというプログラミングが 可能となる。

HTML を文字列として扱っている限りは、変形可能なデータとして扱うことがで きない。そのため HTML(XML) は単なる表現手法の一種としてしか定義できない。この ような手法を導入することによって、初めて HTML(XML) をプログラミングに取り入れ る利点が発生してくる。

わびさび XPath

XPath は、ある XML 文章の一部分を指定するための記法である。階層化ファイル システムでは、例えば「 /etc/passwd 」というように、パスによってある一つ のファイルを指定する。XPath ではその比喩と同じように、一つの XML 文章中の ある一部分をパスによって指定する。例えば上記の HTML 例における ヘッダー要素を指定するときは「 /html/body/h1 」といった表記をする。これは つまりある一つの文字列で、XML 文章の一要素を指示できるということであり、 これによって XML 文章の一部分を取り出すという指示を容易にしている。

qwikWeb では、わびさび方式による配列に対して XPath で一部分を取り出すこと ができるようにしている。 2 上記で見てきたテストケースでは、そのように XPath で XML 文章の一部分を取り出し、それを比較・検証している。 このように簡単に一部分を取り出して検証できるようにするこ とによって、生成した HTML の検証を容易にしている。

また、配列の一部分を取得するだけではなく、 取り出した配列をを破壊的 に変形させることができる。つまりある一つのテンプレートとなる 配列を用意し、そこに必要な部分に必要な要素を代入していくといっ たことができる。つまり、Amrita が実現しているような id によって代入する要 素を指定するといった操作と同等なことを、このわびさび XPath 方式で実現で きる。つまり__「わびさびテンプレート」__として使うことができる。

デザインと構造の分離

ここではプログラム中で HTML を指定する方法について扱っているが、一般に は HTML は別のファイルで指定し、そのうちの一部分をプログラムで書き換え るというテンプレート方式をとることが多い。この HTML による別ファイルを デザイナが書き換え、ページのデザインを作り、プログラマはページの一部分 の書き換えを指定するという形で、デザイナとプログラマの連携をとること が多い。わびさび方式の場合は、このようなデザイナとプログラマの連携は どのように扱われるのだろうか。

このデザインと構造の分離の問題は、非常に難しい問題である。そのような難 しい状況では、まずは原理的に考える必要があると思っている。qwikWeb では、 もっとも原理的な分離方法を考え、それを実践している。 それは__「デザイナは CSS を触り、プログラマは HTML を触る」__という 分離である。HTML はプログラマのみが操作し、その際に、クラスの指定など を適切に行い、後で容易にスタイルを指定できるようにする。そして、全ての デザインは、CSS のみで指定する。このようにすれば、プログラマは HTML を 操作し、デザイナは CSS を操作するという形で両者の連携をとれるようになる。

これは実際のところ理想論であり、デザインと構造の分離を考える際の、一つ の極北だろう。しかし qwikWeb では実際にこれをやっている。いくつかのペー ジデザインをデザイナに依頼して CSS のみでデザインしてもらったが、なん とか実現できた。これは短期的には非常に大変だが、しかし長期的にはこのよ うな理想論こそが生き残ると信じている。

また、qwikWeb では部分的にテンプレート方式も取り入れている。 qwik/lib/template 以下に、HTML によるページ・テンプレートが存在しており、そ れを配列に変換して最初に読み込む仕組みになっている。そのテンプレート中 の class=”section” などと指定された個所が書きかわることによって最終的 なページが指定される。このように、部分的には Amrita と同様に HTML ファイル による指定との連携をとることができる。

ジェームズ・クラーク式記法

※2017年10月19日(木)22:14 るびま編集による歴史的追記。 この節で使っているような、 要素のタグ間の文字列に改行などの空白が入るのを防ぐ記法を、 「ジェームズ・クラーク式記法」乃至「James Clark 記法」と呼ぶことは、 実は結果的には勘違いによって、 この記事の著者である江渡さんが作ってしまったものであると、 後に確認されました34。 なお、あまり多くは使われてはいないフレーズのようですが、 英語圏で James Clark notation というフレーズは、 「{http://www.w3.org/2000/xmlns/}xmlns」 という ようにして名前空間プレフィックスのURIを展開した記法のことを指して使われていることが あります5。 ※るびま編集による歴史的追記(ここまで)

上記のコードを実行すると、下記のような出力が得られる。

<html
><head
><title
>hello</title
></head
><body
><h1
>hello, world!</h1
><p
>This is a <a href="hello.html"
>hello, world</a
> example.</p
></body
></html
>

このように、タグの最後の「 > 」の手前に改行がくるという記法で出力される が、これはバグではなく、意図的にそのように出力している。 これを__「ジェームズ・クラーク式記法」__という。 一般的な記法ではタグの前後に改行をいれて出力す るが、ジェームズ・クラーク式記法ではタグの前後ではなく、あえてタグ内に改行を いれる。

これは、XML における意味論的な複雑さを回避するためである。 一般に XML では、タグの前後にある空白を無視することと決められている。 しかし例えば pre 要素のように要素内の改行を 正しく扱う必要がある場合もある。つまり、ある場合はタグの前後にある改行を 無視し、ある場合には正しく認識する必要がある。XML を出力するルーチンが自動的にタグの前後に改行を入れてしまった場合、 その改行を要素の一部 として認識するべきかどうかについて、パーサにおいて混乱が生じる可能性がある。

ジェームズ・クラーク式記法の場合は、要素間、タグ間には改 行を加えないようにして、タグ内に改行を加えることにより、出力された XML の読みやすさを保ちつつ、パーサにおける複雑さを回避している。

ジェームズ・クラークはご存じの通り、XML という規格を作り出した一人であ り、RELAX NGを作りだした人の一人である。つまり、XML 界の超大物であ る。XML における意味論的な複雑さの問題は、実は非常に奥が深い問題であり、 彼の名前が冠された「ジェームズ・クラーク式記法」は、そのような XML にお ける複雑さを解消することを目的としている。 つまり結論を一言で言えば、「ジェームズ・クラーク式記法」はものすごく良い記 法なので、みなさんぜひ採用しましょう、ということである。

わびさび方式の歴史

このわびさび方式は、高林哲氏の記述によれば、 八重樫氏が考えだしたのが最初である。

このような配列方式で XML を扱う手法は、他の言語においても似た形式が存在 している。代表的なのは、scheme における SXML だろう。これは、S 式で XML を扱 う手法である。

このように S 式で XML を扱っている Wiki サーバとして、 WiLiKi があ げられる。

Gonzui では、このわびさび方式を開発 に全面的に取り入れている。この Gonzui での手法に影響を受け、qwikWeb でも わびさび方式を取り入れるようになった。

わびさび方式では、format_xml で最終的に文字列に変換する必要があるが、Ruby のコードだけで実装した場合には処理に時間がかかり、パフォーマンス上の問 題が生じる。Gonzui において、format_xml はC言語による拡張として書き直さ れ、非常に高速化された。

わびさび方式の拡張

Gonzui における format_xml では、必要最小限の要素についてのみ対応しており、 コメントや、CDATA を扱うことはできなかった。そのため、qwikWeb ではそのよ うな要素も記述できるように拡張をした。また、xml 宣言や DOCTYPE 宣言につい ても柔軟に扱えるように拡張した。それぞれのわびさび方式における記法と、 XML における表現の対応一覧をあげる。

[:'?xml', '1.0', 'utf-8', 'yes']
↓
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
[:'!DOCTYPE', 'html', 'PUBLIC', '-//W3C//DTD html 4.01 Transitional//EN',
  'http://www.w3.org/TR/html4/loose.dtd']
↓
<!DOCTYPE html PUBLIC "-//W3C//DTD html 4.01 Transitional//EN"
  "http://www.w3.org/TR/html4/loose.dtd">
[:'!--', 'comment']
↓
<!--comment-->
[:'![CDATA[', 'cdata']
↓
<![CDATA[cdata]]>

わびさび方式の利点と欠点

さてこのように利点だらけのわびさび方式であるが、欠点もある。 一つは、「遅い」という欠点である。操作を配列の操作として行っている ため、文字列の操作だけに比べると遅くなる可能性がある。また、 ツリー構造をトラバースするような処理を使うと、遅くなる。 つまり、最後の HTML としての書き出しは結構遅いが、 これは現在は C 言語による拡張として書かれているため、大きく改善されている。

もう一つは、「慣れてない」という欠点である。 いままでは、HTML を出力するといえば、 文字列で組み立てていくのが一般であり、 いままで慣れ親しんできた HTML の扱い方からは大きく異なる。 このような「慣れ」に纏わる状況が変化するには非常に長い時間がかかるだろう。

逆に言えば、慣れてないということ以外には、ほとんどまったく欠点は ない。この「わびさび方式」を採用することによって、プログラムを より書き易くなり、よりセキュアになることは確実である。 この「わびさび方式」は、絶対の自信をもってお勧めすることができる。 みなさんぜひ「わびさび方式」を採用しましょう。

qwikWeb の基本的構造

さて、まず qwikWeb の一つの特徴である「わびさび方式」について解説した。 次に、qwikWeb がどのような構造になっているのか、その基本的な構造を見てみる。

qwikWeb は、WEBrick 上に実装されている。そのため WEBrick 上のインタフェー スに合わせて設計している。

一つのリクエストには、下記 4 つのクラスのインスタンスが与えられる。

  • Config : サーバの設定
  • ServerMemory : サーバの状態を保持するメモリ
  • Request : クライアントからのリクエスト
  • Response : クライアントに返す内容

(全て Qwikモジュール下にある。つまり全て前に Qwik:: がつくが、省略している。)

まず ConfigServerMemory は、 サーバが常に保持しているものである。Config はサーバ全体の動作を決める設定が入っており、サーバ動作中は変更されない。 ServerMemory はサーバが動作中に保持する情報を扱う。 一つのサーバにつき、それぞれ一つのインスタンスを保持する。

Request インスタンスはクライアントからのリクエストを抽象化したイ ンスタンスであり、WEBrick::HTTPRequest による情報を元に生成される。 Response インスタンスは、クライアントに返す内容を保持するインス タンスである。そしてこの 4 つのインスタンスを元に、Action という インスタンスが生成される。Action#run というメソッドにて、Request に指 示された内容を元に様々な処理が行われ、結果として Response インスタンス が変形され、クライアントに返される。 6

つまり、一つのセッションは、 __Request インスタンスを受け、Response インスタンスを返す__という 一つの形に抽象化されている。このような抽象化を行うことの最大の利点は、 非常にテストがしやすくなるということである。Web アプリケーションは一般 にテストを書くのが面倒である。ネットワークを介して動作するシステムであ るため、クライアントはネットワークを介してリクエストを送信し、その結果 をネットワークを介して受け取る。しかし、このようにリクエストとレスポン スをオブジェクトのレベルで抽象化すると、Request インスタンスを生成して 処理を行い、結果となる Response インスタンスを検査することで、容易に動 作検証を行うことができる。

サーバ側で保持する情報は、全て ServerMemory が保持する。例えばページの 編集をするとサーバ側での状態の変化が起るが、そのような状態の変化は全て ServerMemory が保持している。ServerMemory の中には、Farm インスタンス があり、これが複数の Wiki サイトを一つに束ねる役目をしている。一つの Farm は Site という Wiki サイトを表す複数のインスタンスを保持している。 Site は Page という複数のインスタンスを持つ。

Action の構造

基本的な一つのセッションは、Request を受け取り、Action で処理し、 Response を返すという構造になっている。Request と Response は比較的単 純なオブジェクトだが、Action はどうなっているだろうか。実は Action ク ラスは、Request を受け取って処理するのに必要な全てのメソッドを保持して いる。つまり、qwikWeb が行う処理においては全て一旦この Action というクラスのメ ソッドが実行され、その Action の中で様々なメソッドを行ったりきたりしな がら処理され、結果が返される。ページを表示する、ページを編集するなどと いった基本的な操作も含め、全ての操作やプラグインは、この Action という 一つのクラスの中に存在する。つまり、Action はキッチンシンクになっている。 7

具体的に Action がどのように実装されているかを見てみよう。まず、最も単純 な形のプラグインのコードを示す。

  • qwik/lib/qwik/act-sample.rb
$LOAD_PATH << '../../lib' unless $LOAD_PATH.include?('../../lib')

module Qwik
  class Action
    def plg_hello(target='world')
      return [:strong, "hello, #{target}!"]
    end

    def act_hello
      c_notice('hello, world!') {
        'hi, there.'
      }
    end
 end
end

if $0 == __FILE__
  require 'qwik/test-common'
  $test = true
end

if defined?($test) && $test
  class TestActSample < Test::Unit::TestCase
    include TestSession

    def test_plg_hello
      ok_wi([:strong, 'hello, world!'], '{{hello}}')
      ok_wi([:strong, 'hello, qwik!'], '{{hello(qwik)}}')
    end

    def test_act_hello
      t_add_user
      res = session('/test/.hello')
      ok_xp([:title, 'hello, world!'], '//title')
      ok_in(['hi, there.'], '//div[@class="section"]')
    end
  end
end

大きく 2 つのパートにわかれている。 上の部分がプラグインを定義している部分である。 下の部分は、テストケースである。 まず上の部分から見てみよう。

module Qwik
  class Action
    def plg_hello(target='world')
      return [:strong, "hello, #{target}!"]
    end
  end
end

ここでは、hello というプラグインを定義している。hello プラグインはどのよ うな動作をするのかというと、Wiki の文中に、{{hello}}と記述する と、その個所が hello, world! におきかわる。つまり hello プラグインは、 strong タグにかこまれた hello, world! というテキストを表示するプラグイン である。このメソッドは、引数を一つとることもできる。 {{hello(qwik)}} と記述すると、hello, qwik! におきかわる。

つまり、Action に__「 plg_ 」__で始まるメソッドを定義すると、それがプラグイ ンの定義を意味することになる。わびさび方式による配列 として HTML を作成し、返り値としている。

さて、次のコードを見てみる。

module Qwik
  class Action
    def act_hello
      c_notice('hello, world!') {
        'hi, there.'
      }
    end
  end
end

_「 act 」__で始まるメソッドは、アクションと呼んでおり、Wiki サイトにおけ る行動を意味する。

下記の URL にアクセスしてみよう。

アクセスすると、タイトルに「 hello, world! 」と表示され、 本文に「 hi, there. 」と表示される。

これはつまり、「 hello 」というアクションが呼ばれることによって、 テンプレートとして notice ページを用意し 8、 タイトルを「 hello, world! 」に、 本文を「 hi, there. 」にするということを指示している。 この本文の個所にはとりあえず文字列だけを指定しているが、 実際にはわびさび方式によって HTML を記述できる。

このように、ある URL に対してあるアクションが起きるということも、 この Action クラスのなかに、並列して存在している。

実はもう一種類、エクステンションという種類も存在する。「 ext_ 」 で始まるメソッドは、エクステンションと呼んでおり、ある一つのページに対 する行動の指示である。例えば FrontPage を表示する際の URL を見てみる。

この URL は、一見 FrontPage.html という静的なファイルの取得を意味している ように見えるが、そうではなく、実はこれも動的な処理 である9。 この URL の拡張子は「 html 」 であるが、これは「 FrontPage 」というページを対象とし、「 ext_html 」というメ ソッドを呼び出すという意味になる。

いくつかのエクステンションの例をあげる。

このように、FrontPage というある一つのページに対して、様々な操作を 定義することができる。

このように、メソッドの定義がそのままプラグインやアクションやエクステン ションの定義となっている。メソッド名がそのままプラグイン名や URL にマッ ピングされる仕組みとなっており、これによって機能追加を容易にしている。 また、ファイルの読み込みも自動化されている。qwik/lib/qwik の中に「 act- 」か ら始まるファイルがある場合は、自動的にロードする。つまり「 act- 」から始 まる新しいファイルを作れば、自動的にそれも読み込まれる。

Action クラスが保持するメソッドをまとめておく。

  • 「 plg_ 」 文中にうめこまれるプラグイン
  • 「 ext_ 」 あるページを対象として処理を行うエクステンション
  • 「 act_ 」 サイト全体を対象とするアクション

それ以外には、「 c_ 」(c は common の略)で始まる共通に使われるメソッ ドがある。

テスト重要

コードとテストの共存

さて先程はコードの上半分において、実際にプラグインやアクションが どのように Action というクラスの中に存在するのかを見てみた。次に、下半分 のテストケースを見てみる。これが qwikWeb の特徴的な面の一つであるが、 テストケースも同じ一つのファイルの中に共存している。個々のファ イルは全て、一つの Ruby スクリプトとして実行可能となっており、実行した場 合にはそのファイルに含まれるコードの単体テストが実行される。

まずは試しに、テストを一つ実行させてみよう。 下記のように何かファイルを一つ実行させてみてほしい。

% cd lib/qwik
% ruby act-sample.rb
Loaded suite act-sample
Started
....
Finished in 0.254036 seconds.

4 tests, 10 assertions, 0 failures, 0 errors

このように、テストが実行されるはずだ。ここでは 4 tests, 10 assertions が 実行されたが、これはつまり act-sample.rb に含まれる機能のみをテストして いるということである。

実際にコードを見てみよう。上半分の「 module Qwik 」で囲まれた領域はプ ラグインやアクションの定義だが、下半分はテストの時のみ実行される コード(テストケース)である。

if $0 == __FILE__
  require 'qwik/test-common'
  $test = true
end

ここではまず、そのコードが単体で実行されているかどうかを判定する。コー ド単体で実行された場合には、テスト実行時に共通で使う test-common を require し、$test を真にする。

if defined?($test) && $test
  ...
end

$test が定義され、真の時のみ中のコードが実行される。つまり、単体で 実行された場合はこの中のコードが実行される。 なぜこのように、一旦 $test を真にしてから、次にその $test を判定するとい う二段階方式にしているのだろうか。それは、このファイル単体のテストをで きるようにすると共に、システム一式のテストも行えるようにするためである。 つまり、全てのテストケースを一度に読み込むという使い方もできる。 10

試しにテストスイートを実行してみよう。

% ruby test-suite-all.rb 
Loaded suite test-suite-all
Started
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
..................................
Finished in 33.755652 seconds.

514 tests, 3314 assertions, 0 failures, 0 errors

このように、現在テストスイートには、514 tests, 3314 assertions が含まれ ている。この場合は 33.8 秒かかっているが、テスト件数が多くなると、 このようにテストを実行するのに時間がかかるように なる。しかしテストケースは個々のファイル毎に実行できるため、個々の機能を開発している時は、 それぞれのファイル毎にテストを実行すればよい。 これにより、テストの実行時間は短くなる。

このように、qwikWeb ではコードとそのテストケースを一つのファイルに押し 込めて、一体化させることによって、テストケースとコードをいったりきたり しやすくなり、コードとテストの対応関係がわかりやすくなる。どの機能(メ ソッド)はテストしていて、どの機能はテストしてないかが、すぐにわかるよ うになる。テストを個別のファイルに分離することによって、現在開発中の機 能だけをピンポイントでテストできるようになり、テストのラウンドトリップ タイムを短くすることができるという利点がある。

ブラウザをシミュレートしたテストケースの書き方

実際のテストケースを見てみる。

  class TestActSample < Test::Unit::TestCase
    include TestSession

    def test_plg_hello
      ok_wi([:strong, 'hello, world!'], '{{hello}}')
      ok_wi([:strong, 'hello, qwik!'], '{{hello(qwik)}}')
    end

    def test_act_hello
      t_add_user
      res = session('/test/.hello')
      ok_xp([:title, 'hello, world!'], '//title')
      ok_in(['hi, there.'], '//div[@class="section"]')
    end
  end

新しい TestCase を一つ作り、TestSession モジュールを include している。 このモジュールの setup で必要なオブジェクトの 準備を行い、teardown では全ての環境の消去を行っている。 一つ目は、hello プラグインのテ ストケース、二つ目は hello アクションのテストケースである。

   def test_plg_hello
     ok_wi([:strong, 'hello, world!'], '{{hello}}')
     ok_wi([:strong, 'hello, qwik!'], '{{hello(qwik)}}')
   end

ok_wi というのは、TestSession で定義されたテスト用のメソッドの 一つである。元々は assert_wiki という名前だったが、非常に良く使うので短 くした。右側に Wiki 記法によるテキストを入力し、左側に結果として生成され る HTML を入力する。これはつまり、Wiki ページに {{hello}} という 文字列を記述すると、その結果として左側のような HTML コードが生成されると いうことをテストしている。その次も同様に {{hello(qwik)}} が どのような HTML になるかをテストしている。

このように、__「Wiki ページに何か記入する」→「それが HTML に変換される」__という組み合わせの動作テストを、 __一行で簡単に書ける__ようにしているわけだ。

次に、hello というアクションのテストである。

   def test_act_hello
     t_add_user
     res = session('/test/.hello')
     ok_xp([:title, 'hello, world!'], '//title')
     ok_in(['hi, there.'], '//div[@class="section"]')
   end

まず最初の一行では、ユーザの追加を行っている。qwikWeb は基本的には登録 されたユーザだけがアクセスできる仕組みであり、ここでテスト用のユーザの追加を 行っている。「 t_ 」で始まるメソッドは、TestSession で定義された、 テストで共通に使われるメソッドである。

     res = session('/test/.hello')

この行では、サーバに「 /test/.hello 」というパスでアクセスをしている。つ まり比喩的に言えば、下記のような HTTP アクセスをするのと同じような動作を 指示している。(実際にはネット経由のアクセスではなく、プロセス内で閉じ た動作である。)

% telnet localhost 9190
GET /test/.hello HTTP/1.0

HTTP/1.1 200 OK
Date: Wed, 14 Dec 2005 23:59:15 GMT
Content-Type: text/html; charset=Shift_JIS
Server: qwikWeb/0.7.1+20051002
Content-Length: 1230

.....ここに HTML がくる.....

結果となる res は Response オブジェクトであり、 res.body という形でHTMLを取り出せる。 また、res は同時に @res というインスタンス変数にも代入される。 11

     ok_xp([:title, 'hello, world!'], '//title')

ok_xp も同様に TestSession で定義されたメソッドである。元は assert_xpath と いう名前だったが、短くした。これは、右の引数を XPath として解釈し、 @res.body から XPath でその HTML 文章の一部分を取得し、左側と比較している。 __「 //title 」__というのは XPath で記述した、XML 文章の一部分を指定した内容で あり、HTML 文章のうちの title 要素を示している。

     ok_in(['hi, there.'], '//div[@class="section"]')

この行も先程と同様に、右側が XPath であり、XPath で取得した要素の内部を、 左側と比較する。__「 //div[@class=”section”] 」__は XPath による HTML 文章の一部 分の指定であり、class=”section” となっている div 要素を意味する。 ok_xp は要素全体を比較するが、ok_in は要素の中だけ比較すると いう違いがある。ok_in は ok_xp の派生型であるが、非常によく使うた め、短縮形として用意している。

要するに、ここでのテストは、ネット経由でブラウザがアクセスす る際の挙動をそっくりそのままシミュレートしている。 このようなテストを充実させることによっ て、実際にブラウザで機能テストをしてみなくても、正しく動作することを保 証できるようになる。このように、ブラウザからアクセスする際とほぼ同じ挙 動を示すテストケースを書くことによって、Web アプリケーションのテストが 非常にやりやすくなることは、非常に重要である。

qwikWeb では、実際にこのようなブラウザから見た際の挙動のテストをほぼ全 機能について行うことによって、安定したサービス運営を行えるようになって いる。 12

老人力としてのテスト能力

さて、なぜこれほどまでにテストに力をいれているのか。それは、ここで扱っ ているコードが、実際のサービス運用に使っているコードだからだ。「研究」 用のコード、つまり実際に使う人がいないコードだったら、ここまでテストに 力を入れる必要は無いかもしれない。しかしサービスを提供し、そのサービス を使う人がいるのならば、例えそれが無料のサービスだったとしても、そのサー ビス運営に責任を負うことになる。そのようなサービスを運用するにはそれな りの体制を必要とする。それを一人で全部やるためには、それなりに工夫をす る必要がある。それがここで解説したテストの仕組みである。

このように、テストに要する負担を軽減させ、全ての機能をテストすることに よって、ようやく安定してサービスを続けられるようになった。また、どのよ うにコードを書き換えたとしても、それが他の動作を阻害していないことを自 信を持って判断できるようになった。そのようにして、安定したサービス運営 と、連続的な機能追加を同時に可能にした。実際に、すでに 2 年以上、安定 したサービス運営を続けている。

またもう一つの理由がある。それは、簡単に言えば、テストしないとコードが 書けなくなったからだ。昔は頭の中にコードを格納して、そこで動作確認をし、 デバッグをすることもできた。多少複雑なコードでも、テストも何も無しで、 いきなり運用し始めることができた。でも、今はもうそうもいかない。コード にミスが多くなったし、デバッグもすぐにはできなくなった。つまり、コーディ ング能力がおとろえたことが、テスト能力をみがくきっかけとなったわけだ。

みなさんも「最近、足腰が弱くなった」と嘆くのをやめて、「最近、杖を使う のがうまくなった」という風に、気持ちを切り替えてみてはいかがでしょうか。 そして、何度も転びながら進む若者を後目に、上手に杖を使いこなしながら着 実に先に進む姿というのも、かっこいいかもしれません。

qwikWeb の拡張

ここまでで、Action にメソッドを追加することにより、プラグインやアクショ ンを追加するという qwikWeb の基本的構造を解説した。さて、具体的にどの ようにプラグインを追加するのか。ここでは二つのプラグインの実装の概略を 見てみよう。(詳細について次号以降で扱うため、ここでは概略のみとする。)

滝川クリステルプラグイン

act-sample.rb を用いた解説で、簡単なプラグインの作り方は理解できた。し かし、HTML を出力する方法だけでは面白いプラグインはできないと思われる かもしれない。しかし、HTML の出力だけでも、工夫次第で面白いプラグイン はできる。ここでは、最近話題の 「滝川クリステルジェネレータ」を 題材にして、滝川クリステルを自在に 出力するプラグインを作ってみよう。 名付けて__「滝川クリステルプラグイン」__である。

まず、実際のコードを引用する。

  • qwik/lib/qwik/act-christel.rb
    def plg_christel(width = 320)
      width = width.to_i.to_s
      content = yield
      message, image_url = content.to_a
      message.chomp!
      image_url.chomp! if image_url
      query_str = {
        :m => message.set_page_charset.to_utf8,
        :u => image_url
      }.to_query_string
      url = "http://gedo-style.com/crstl/crstl.php?#{query_str}"
      return [:img, {:src=>url, :width=>width}]
    end

このようにして画像を埋め込むプラグインを実現できる。 (詳細な説明は、次号以降で行う。)

このプラグインが実際に動いている様子は、こちらをご覧下さい。

滝川クリステルジェネレータの作者に感謝いたします。

高橋メソッドプラグイン

もう少し実用的で、複雑な構造を持つプラグイン例として、 __「高橋メソッドプラグイン」__を解説する。これは、Wiki 上で自在に高 橋メソッドによるプレゼンを実現するプラグインである。実際のコードはこち らのファイルをご覧下さい。

  • qwik/lib/qwik/act-takahashi.rb

このコードでは、Flash で表示するのに必要なテキストファイル、表示用 HTML ファイルを作成し、それを iframe でページ中に埋め込んでいる。

実際の表示例はこちらをご覧下さい。

このプラグインでは、WEBLAB@AJIBIT にて公開さ れている「高橋メソッドマシーン」という Flash を使用している。「高橋メソッ ドマシーン」の作者に感謝します。

まとめ

本稿では、qwikWeb を構成する特徴的な開発手法のうち、「わびさび方式」と、 テストを容易にする構造の二点について解説した。二点について骨子をまとめると、 下記のようになる。

  1. わびさび方式を採用することによって、プログラムはより書き易く、よりセキュアになる。
  2. Web アプリケーションを構築する際は、ブラウザからのアクセスと同じようなテストケースを書けるように工夫することによって、システムの安定性を確実に検証できるようになる。

みなさんもぜひこのような工夫によって、安心できる Web アプリケーション構 築を行えるようになりましょう。

次号では、qwikWeb にプラグインを追加する方法の詳細について扱います。 ではまた!

著者について

えと こういちろう (Rubyist)。 Ruby 界の規約に合わせ、今後はひらがなでいくことにした。

漢字の方の江渡 浩一郎はメディア・アーティストとして活動しており、 12 月 25 日まで ICC で行われている展覧会にて、作品を展示している。


  1. 使用頻度から考えれば、Array のメソッドとして定義する必要は無いかもしれない。 

  2. 実際にはここで実装している XPath は、XPath の構文を全てサポートしているわけではなく、テストを行う際に必要だった構文だけをサポートしている。XPath モドキといった方が正確である。 

  3. https://twitter.com/eto/status/391633752 

  4. https://twitter.com/eto/status/391635652 

  5. この件に関する簡単なまとめ → http://ksmakoto.hatenadiary.com/entry/2017/10/15/123716 

  6. 実際には Response オブジェクトの情報から WEBrick::HTTPResponse インスタンスへと書き戻され、それを WEBrick がクライアントに返す。 

  7. キッチンシンクアプローチとは、Emacs エディタに代表されるような、さまざまな機能を一箇所につめこむような設計を意味する。 

  8. テンプレートには他に c_surface, c_plain などが存在する。 

  9. 正確には、静的な HTML を生成してそれを返す場合もある。ページを見るという動作は非常に頻繁に発生するため、ゲスト、つまりログインしていないユーザとしてアクセスする場合は、静的なページを返している。 

  10. ここで $test という一般的な変数名を選んだのは失敗だったと思っている。この仕組みを理解していない人が、たまたま $test をセットしてから require すると、謎のエラーが発生していまう。グローバル変数の変数名はもっと考えてつけるべきである。 

  11. 並列実行のテストといった特殊な状況ではインスタンス変数を使えないため、返り値を使って検証する必要がある。 

  12. 実際にはまだテストできない項目がある。CSS のテスト、JavaScript のテストは、この方式では対応できない。今後の課題である。