ソースクラスとレンダラクラス

テーブルやスクローラはロジックが複雑なうえに、そのデザインには恐ろしくさまざまなバリエーションがありうる。 生半可なカスタマイズ性では対応できない。そのため、テーブルやスクローラを描画する(HTMLを吐く)ためのクラスを、デザインに応じて書くべしと決めた。 これが「レンダラクラス」である。なおレンダラクラスのインスタンス化は jspbp-renderer が行う。

(ロジックが単純なものは JSP ファイルに書いてしまうことを想定している)

ドメインオブジェクトをそのままレンダラオブジェクトに渡して描画させるのでは、レンダラクラスがドメインモデルに 密結合してしまい、汎用性に欠ける。だから、レンダラクラスが描画するデータモデルは、レンダラクラスが指定する。 これが「ソースクラス」である。ドメインオブジェクトからソースオブジェクトが生成されてレンダラオブジェクトに渡る、という筋書きになる。

まとめると、データの流れは以下のとおり:

ドメインオブジェクト → ソースオブジェクト → レンダラオブジェクト → HTML

ドメインオブジェクトからソースオブジェクトを生成する作業は jspbp の behind でやるといい。

ビューステートとクエリ文字列

JSF と ASP.NET を使ってみたところ、フレームワーク自体がろくでもないハックをやっていることに気がついた。 JavaScript を使ってハイパーリンクを POST にすりかえているのだ。賛成できない。 破壊的操作を行うのは常にボタンであるべきだし、非破壊的なら GET であるべきだ。 非破壊的というのはたとえば、

  • テーブルをカラム指定でソートする
  • テーブルをスクローラで分割表示する

というような、ビューの状態(命名:ビューステート)に関することだ。なお、ASP.NET のビューステートはセッションの類似物であり、 ビューの状態に制約されていない仕組みなので、混同なきよう。

というわけで、jspbp-renderer はビューステートをクエリ文字列として格納する。

ひとつのページ中に存在するテーブルは複数ありうるので、ビューステートには名前をつける必要がある。これが viewStateId だ。 viewStateId はクエリ文字列の name として使われる。たとえば viewStateId が testTable なら、 カラム「FirstName」で昇順にソートするクエリ文字列は以下のとおり:

testTable=s.aFirstName

「s.aFirstName」の頭3文字の s はソート、a は昇順を意味する。

ソートに加えて、スクローラがオフセットを指定するなら以下のとおり。

testTable=s.aFirstName&testTable=o.20

renderer タグ

レンダラクラスの名前と viewStateId とソースを結びつけるのが、 JSP ファイル中の r:renderer タグだ。

<%@ taglib uri="http://kaoriha.org/jspbp/renderer/20071102" prefix="r" %>
<r:renderer rendererClass="org.kaoriha.jspbp.renderer.table.Table" viewStateId="testTable" source="${behind.dataSource}"/>

org.kaoriha.jspbp.renderer.table.Table はレンダラクラスのサンプルとして jspbp-renderer の jar に同梱されている。

ここで注意すべき点がひとつ: viewStateId はあくまでビューステートの ID であり、レンダラオブジェクトの ID ではない。

レンダラクラスを書く

org.kaoriha.jspbp.renderer.table.Table クラスを例にとって説明する。

レンダラクラスは AbstractRootRenderer<T> クラスを継承する。型パラメータの T がソースクラスである。Table クラスの場合、 org.kaoriha.jspbp.renderer.table.Source クラスを型パラメータに取っている。ソースオブジェクトは renderer タグにより injection され、 source フィールドで参照できる。

テーブルは多数の子要素から成る。どのように子要素を作ってもよいが、Table クラスは子要素をそれぞれ別のオブジェクト(子レンダラ)に対応させている。 子孫レンダラはAbstractRenderer<T> クラスを継承する。これは利便性のためのクラスであり、使用は必須ではない。 なお、すべての子孫レンダラがビューステートを共有していることに注意。

子孫レンダラはすべて、親レンダラのコンストラクタ内でインスタンス化される。こうすることにより、既存のレンダラクラスを継承してカスタマイズするときには、 子孫レンダラを部分的に入れ替えることができる。サンプル中でも、Thead クラスが Tr クラスの子レンダラ(デフォルトでは Td クラス)を入れ替えている。

子孫レンダラは親要素を利用できる(AbstractRenderer の render メソッド参照)。HTML の性質上、親要素に属性を追加したいケースが多いので、 この性質は重要である。

コンストラクタが子孫レンダラのツリーを構築したあとは、render メソッドが実際に HTML を組み立てて返す。 Table クラスの reder メソッドは、スクローラやソートを実現するため、まずビューステートからクエリを組み立ててソースオブジェクトに投げている。 そのクエリの結果が実質的なソースオブジェクトとなる。ソースオブジェクトは DTO (Data Transfer Object) として考えてしまいやすいが、Adapter パターンの Adapter でもありうる。もしテーブルのソースオブジェクトを DTO にしたら、巨大なソースオブジェクトを生成してごく一部しか表示しない、という非効率なものになるだろう。