この翻訳記事は、もともと私の個人サーバ上に設置していたブログで公開していたものです。個人サーバを撤去した関係で一時Tumblrへと転載していましたが、このたび、はてなブログへ再転載しました。投稿日付はTumblrへ転載した際の日時です。Tumblrに載せているものはいずれ削除する予定です。
Carsten Hufe氏がブログにて公開している「Apache Wicket - Best Practices (English)」の翻訳許可を御本人から頂きましたので、日本語化しました。Apache Wicketを使っていく上で、とても重要なことがまとめられている、Wicket開発者必読の記事です。特に「常にモデルを使え」というプラクティスは、Wicketのパワーをちゃんと生かすためにには必須のプラクティスです。
ぜひ一読して、今後の開発に生かしてください。
Apache Wicketはここ最近とても人気で、多くのプロジェクトにも採用されている。Wicketのパワーを使えば、とても簡単に、しかも速く、開発を進められるのだが、Wicketの使い方は幅が広く、同じ機能を実現するにも多くのやり方がある。この記事では、Apache Wicketの、正確かつ効果的な使い方レシピを紹介したい。
この記事の想定する読者は、Apache Wicketを既に使ったことのある人だ。Wicketを使い始めたばかりの開発者は、さまざまな面で難しいと感じることにぶつかるだろう。それは、よくあるJSFやStrutsのパターンやアプローチでWicketを使おうとするからだ。JSFやStrutsといったフレームワークの使っている手法は、原則的に手続き的だ。Wicketは対照的に、オブジェクト指向に強く寄っている。だから、まずJSFやStrutsのパターンを忘れるところから始めないと、これから先Wicketで楽しく開発していくことはできない。
コンポーネントを正しくカプセル化せよ
コンポーネントは自らについてちゃんと知っているべきだ。コンポーネントの利用者が、コンポーネントの内部実装を知る必要も、気にする必要もないように作ろう。利用者が、コンポーネントのドキュメンテーションと外部インタフェースを知るだけで良いようにしよう。もう少し細かく考えてみよう。例えば、WicketのPanel型コンポーネント(もちろんPanelも含めて)を継承したコンポーネントには、対となるHTMLテンプレートがある。一方、WebMarkupContainerやFormといったクラスを継承したコンポーネントには、HTMLテンプレートはない。だから、WebMarkupContainerやFormを使うときには、コンポジションパターンに従い、他のコンポーネントをaddする必要がある。
public class RegistrationForm extends Form<Registration> {
public RegistrationForm(String id, IModel<Registration> regModel) {
super(id, new CompoundPropertyModel<Registration>(regModel))
add(new TextField("username"));
add(new TextField("firstname"));
add(new TextField("lastname"));
}
}
リスト1
リスト1は、悪いコンポーネントの定義例だ。RegistrationFormの利用者は、このコンポーネントの内部構造を知らなければ使うことができない。
public class RegistrationPage extends Page {
public RegistrationPage(IModel<Registration> regModel) {
Form<?> form = new RegistrationForm("form");
form.add(new SubmitButton("register") {
public void onSubmit() {
}
});
add(form);
}
}
<html>
<body>
<form wicket:id="form">
Username <input type="text" wicket:id="username"/>
First name <input type="text" wicket:id="firstname"/>
Last name <input type="text" wicket:id="lastname"/>
<input type="submit" wicket:id="register" value="Register"/>
</form>
</body>
</html>
リスト2
リスト2は、RegistrationPageで実際にこの出来の悪いコンポーネントを使ってみた例だ。入力フィールド firstname、lastname、username に注目して欲しい。これらのHTMLフィールドに対応するコンポーネントは、RegistrationForm内でaddされているので、RegistrationPageではaddされていない。このような、他の開発者がコンポーネントがどこでRegistrationPageにaddされたのか分からなくなるようなコンポーネントの作り方は避けよう。
public class RegistrationInputPanel extends Panel{
public RegistrationInputPanel(String id, IModel<Registration> regModel) {
super(id, regModel);
IModel<Registration> compound = new CompoundPropertyModel<Registration(regmodel)
Form<Registration> form = new Form<Registration>("form", compound);
form.add(new TextField("username"));
form.add(new TextField("firstname"));
form.add(new TextField("lastname"));
add(form);
}
}
<html>
<body>
<wicket:panel>
<form wicket:id="form">
Username <input type="text" wicket:id="username"/>
First name <input type="text" wicket:id="firstname"/>
Last name <input type="text" wicket:id="lastname"/>
</form>
</wicket:panel>
</body>
</html>
リスト3
リスト3では、入力項目をきれいに切り出した。この例では、コンポーネントはPanelを継承しているので、専用のマークアップファイルがある。さらに、この例はWicketにおけるFormオブジェクトの正しい使い方も示している。コンポーネントはformコンポーネントのインスタンス変数に対し、form.add(Component)を使ってaddされるほうが良い。そうすれば、インスタンスがあるので、あとからさらにビヘイビアやバリデータをaddすることもできる。コンポーネントと違ってビヘイビアやバリデータには対応すべきHTMLタグなどはないので、自由に追加可能だ。
public class RegistrationPage extends Page {
public RegistrationPage(IModel<Registration> regModel) {
Form<?> form = new Form("form");
form.add(new RegistrationInputPanel("registration", regModel);
form.add(new SubmitButton("register") {
public void onSubmit() {
}
});
add(form);
}
}
<html>
<body>
<form wicket:id="form">
<div wicket:id="registration">
Display the RegistrationInputPanel
</div>
<input type=”submit” wicket:id="register" value="Register"/>
</form>
</body>
</html>
リスト4
リスト4は、RegistrationInputPanelの使い方を示している。このコンポーネントが内包している子コンポーネントは、パネル自体に直接addされているので、ページのコードからは消えた。RegistrationPageは、RegistrationInputPanelの持つFormとは別の、独自のFormオブジェクトを持っている点に注目して欲しい。このような構造にしても、Formオブジェクトはコンポーネント階層をたぐり、ネストしたFormのサブミット処理を呼び出してくれる。
モデルとページデータはフィールドに格納せよ
Strutsとは異なり、Wicketのページやコンポーネントはシングルトンではない。ページやコンポーネントはステートフルであり、セッションに結びついている。だから、ユーザ固有の情報は、セッションではなく、ページやコンポーネント内部に格納できるのだ。情報はフィールドに格納すべきだ。そうしておけば、コンポーネント内の各メソッドのメソッド引数にこの情報を追加して各メソッドへ次々と持ち回る必要も無く、どこからでもアクセスできる。コンポーネントのインスタンスは、複数のリクエストにまたがって存在できる。例えば、フォームをもつページは、表示も、その後のサブミットや入力検証も、同じページインスタンス上で実行する。さらに、ユーザがブラウザのバックボタンを押して同じフォームをサブミットしたときも、やはり同じページ・インスタンスが使われるのだ。コンストラクタで受け取った値は、コンポーネントのフィールドに格納するのがいいだろう(この時、フィールドはなんらかのIModelにすべきだ)。フィールドに値を格納する場合、値がSerializableかどうかにも注意しなければならない。Wicketでは、ページはページマップに格納され、初期設定では、ページマップはハードディスクに出力されるからだ。Serializableでないオブジェクトがフィールドに格納されたままだと、NullPointerExceptionやNonSerializableExceptionの原因となる。
さらに、(バイナリデータのような)巨大なデータは、フィールドに直接格納すべきではない。パフォーマンスを劣化させるだけではなく、オブジェクトの直列化と復元の際に、メモリリークを起こす可能性がある。巨大なデータを扱う場合には、LoadableDetachableModelを使うといいだろう。LoadableDetachableModelなら、フィールドに格納しても安全だ。このモデルには、データのロードと開放をうまく行う機構が組み込まれている。
Wicket IDを正しく名前づけせよ
名前づけは面倒で邪魔だと思う開発者は多い。しかし、名前づけこそ、ソフトウェア開発における重要なトピックのひとつだと思う。正しく名前づけされていれば、ソフトウェアコンポーネントのビジネス的な側面を容易に推察出来る。さらに、正しく名前づけしていれば、無駄で意味のないコメントを付ける必要も無くなる。
Wicket IDにおける悪い名前づけというのは、例えば、birthdateTextFieldとか、firstNameField、addressPanelといった類の名前だ。なぜこれが悪いかというと、この名前には、2つの別の側面が混ざっているからだ。技術的な側面の名前「TextField」と、ビジネス的な側面の名前「birthdate」が混ざっているのだ。Wicket IDにはビジネス的な側面から名前を付けるのがよい。技術的な側面は、HTMLテンプレートで <input type="text" /> という形ですでに表現されているし、Javaコード側でも new TextField("birthdate") という形で、一目瞭然だからだ。技術的な側面はもう表現されているのだから、いらないのだ。さらに言えば、不適切な名前づけは、技術的なリファクタリングを大きく妨げることがある。例えば、TextField を DatePicker で置き換えるときはどうなる? birthdateTextField を birthdateDatePicker に変更しなければならなくなる。Wicket IDに技術的な側面の名前を使わない理由には、ほかにも、CompoundPropertyModel がある。このモデルは、自身のプロパティを子コンポーネントへと委譲する。この時、委譲を受ける子コンポーネントのWicket IDは、CompoundPropertyModelのプロパティ名と一致しなければならない(リスト3を参照)。リスト3の例では、usernameというWicket IDのTextFieldは、RegistrationオブジェクトのsetUsername()やgetUsername()メソッドを自動的に呼び出す。ここでメソッド名がsetUsernameTextField()でなければならなくなることを考えれば、技術的側面とビジネス的側面の混ざった名前が実用的でない理由がよくわかるはずだ。
コンポーネントツリーの改変を避ける
Wicketのコンポーネントツリーは、静的で動かない骨組みだと考えるべきだ。この骨組みは、モデルにデータが注入されることで始めて意味を持つのだ。ちょうど脳のないロボットの様なものだ。脳を持たないロボットは何も出来ない、ただの死んだ、動かない骨組みにすぎない。しかし、ひとたびロボットにデータを注入すれば、骨組みは動きだす。ロボットにデータを注入するのに、部品を交換する必要はないはずだ。同じく、コンポーネントは、注入されたデータを見て、その状態をもとに自分で判断が出来る。例えば、コンポーネントは自分の可視属性を見て、自分が表示されるべきなのかどうかを判断できるのだ。Wicketでは、コンポーネントツリーを直接操作するのは最小限にすべきだ。つまり、Component.replace(Component) や Component.remove(Component) といったメソッドを呼ぶことは徹底的に避けるべきだ。これらのメソッドを呼ぶと言うことは、Wicketのモデルの使い方を誤ってる可能性が高い。さらに、コンポーネントツリーの構築は、条件に拠るべきではない(リスト5を参照)。コンポーネントツリーの構築が条件によって異なると、同じインスタンスの再利用が大きく妨げられてしまう。
if(MySession.get().isNotLoggedIn()) {
add(new LoginBoxPanel("login"))
}
else {
add(new EmptyPanel("login"))
}
リスト5
LoginBoxPanelの構築を条件に拠って変えるのではなく、パネルは常にaddし、LoginBoxPanelの中でsetVisibilityAllowed(boolean)メソッドを使って、パネルが見えるかどうかをコントロールするほうが良い。そうすれば、LoginBoxPanelが表示されるかどうかは、LoginBoxPanelコンポーネント自身の責務となる。すばらしい! ビジネスロジックはきれいにカプセル化された。外部からの判断は必要なく、コンポーネント自身がすべてのロジックを制御している。この点については「コンポーネントの可視性を正しく実装する」の節にてより詳しい例を紹介する。
コンポーネントの可視性を正しく実装する
サイトの部品が見えるか見えないかを制御するのは、重要なトピックのひとつだ。Wicketでは、コンポーネントの可視性はisVisible()メソッドとsetVisible()メソッドで制御できる。このメソッドはWicketのComponentクラスに定義されており、Componentはすべてのコンポーネントのベースクラスなので、あらゆるコンポーネントはもちろん、ページにもこのメソッドがある。では、LoginBoxPanelを使った例を見てみよう。このパネルは、ユーザがまだログインしていない時だけ表示される。
LoginBoxPanel loginBox = new LoginBoxPanel("login");
loginBox.setVisible(MySession.get().isNotLoggedIn());
add(loginBox);
リスト6
リスト6は悪い実装の例だ。この例では、コンポーネントの可視性をインスタンスを作る過程で決めている。もう一度繰り返しておくが、Wicketのコンポーネントは複数のリクエストをまたがって生き続ける。LoginBoxPanelのインスタンスを複数のリクエストにまたがって再利用しようとすれば、このままでは、あなたがどこかで loginBox.setVisible(false) を呼び出さなければならない。このコンポーネントは、ユーザがログインしたあとは表示されないからだ。これでは不便だ。このコンポーネントを使うには、使う側が適切に setVisible() メソッドを呼んで可視性を制御しなければならない。このままでは、似ているけども実際には異なる状態を絶えず同期していかなければならなくなる。この例では、「見える」という状態は「ログインしていない」という別の状態と結び付いている。「ログインしていない」という状態はビジネス的な状態であり、「見える」という状態は技術的な状態だ。そして、両者常に一致するはずだ。ふたつの状態を別々に管理する代わりとして、最近までは、isVisible() メソッドをオーバーライドする手法が有効であり、私もそう勧めてきた。しかし今は、もうこの手法は勧めない。isVisile()がいつ、何回呼ばれるかは予測がつかず、それが別の問題を生むことがあるからだ。
ふたつの状態をきちんと同期するには、onConfigure() メソッドをオーバーライドして、その中で setVisibilityAllowed(boolean) を呼びだすという手法を勧める。ここで setVisible() ではなく setVisibilityAllowed(boolean) を使うのは、 setVisibilityAllowed() メソッドは final と宣言されており、誰もオーバーライド出来ないからだ。次のダイアグラムを見て欲しい。このダイアグラムは、メソッド呼び出しのフローを表したものだ。onConfigure()とsetVisibilityAllowed()を使う方式に従えば、3つのメソッド呼び出しを省略でき、単に LoginBoxPanel をインスタンス化するだけで済んでしまう。
public class LoginBoxPanel {
@Override
protected void onConfigure() {
setVisibilityAllowed(MySession.get().isNotLoggedIn());
}
};
リスト7
リスト7では、可視性の制御がリスト6から反転しているのが分かるだろうか。ここでは、LoginBoxPanel自身が自分の可視性を決定しているのだ。onConfigure() が呼ばれると、ログイン状態を反映して、可視性が自動的に決まる。2つの状態が同期していないことはあり得なくなった。コンポーネントのレンダリング時にonConfigure()によって状態が同期されるからだ。ロジックは1行のコードに局所化され、アプリケーション全体に散らばることはなくなった。さらに、このコードならば、「見えるかどうか」という技術的側面が、「ログインしているかどうか」というビジネス的側面とつながっているということが、一目瞭然だ。同じルールが、isEnabled() メソッドにも使える。isEnabled() メソッドが偽を返すと、コンポーネントはグレーアウトされる。しかし isEnabled() については、setEnabledAllowed() というメソッドはないので、setEnabled() メソッドを使うところは異なる。非アクティブな、あるいは不可視のコンポーネント内にあるフォームは、単に実行されない。コンポーネント内部ではなく、外部から setVisibilityAllowed() メソッドを呼ばざるを得ないこともあるだろう。例えば、ユーザがボタンを押した結果として、登録フォームがインライン表示される、というケースだ。一般的に、次のルールに従えばいいだろう。コンポーネントの状態がデータに依存する場合は onConfigure() をオーバーライドして setVisibilityAllowed() を呼び出し、ユーザの操作によって起こるイベントに因る場合は onClick() のようなメソッド内で、外部から setVisibilityAllowed(boolean) を呼び出すのだ。最後に、コンストラクタ途中でインスタンス化したコンポーネントの可視性を、外部から制御することは避けるべきだ。匿名クラスを使えば、メソッドをオーバーライドして可視性を制御できる(リスト8を参照)。
new Label("headline", headlineModel) {
@Override
protected void onConfigure() {
String headline = getModelObject();
setVisibilityAllowed(headline.startWith("Berlosconi"))
}
}
リスト8
Note: この節は2011年10月15日に書き替えた。可視性はonConfigure()内でsetVisibilityAllowed(boolean)を使って設定すべきで、isVisible()をオーバーライドすべきではない。読者のフィードバックを受けて改訂した。ありがとう。
常にモデルを使え
常にモデルを使え! 生のオブジェクトを直接コンポーネントに渡すべきではない。ページやコンポーネントのインスタンスは複数のリクエストにまたがって生き続けるが、ここに生のオブジェクトを使っていたら、後からそのオブジェクトを別のものに置き換えることができなくなる。LoadableDetachableModelによってリクエスト毎にロードされるエンティティを考えてみて欲しい。もしエンティティを直接コンポーネントに渡していると、エンティティ・マネージャが新しいオブジェクト参照を作り出しても、コンポーネントは廃れたオブジェクト参照をキープし続けることになる。コンポーネントのコンストラクタ引数には、常に IModel を渡すべきだ(リスト9参照)。
public class RegistrationInputPanel extends Panel{
public RegistrationInputPanel(String id, IModel<Registration> regModel) {
}
}
リスト9
リスト9の方法であれば、コンストラクタはモデルのどんな実装でも受け取ることができる。Modelクラスはもちろん、PropertyModelであろうと、値を自動的にロードして永続化するLoadableDetachableModelの独自実装であろうと、なんでもだ。モデル実装は容易に置き換え可能だ。モデルのユーザとして知っておくべきことは、IModel.getObject() を呼び出すと、Registration型のオブジェクトが手に入る、ということだけだ。オブジェクトがどこから来るのかは、呼び出しコンポーネントやモデルの実装によって変化するだろう。モデルを使わないと、そのうち、モデルを使わない代わりにコンポーネントツリーを改変しなければならなくなるだろう。そうすると、状態を複製して持ち回らなければならなくなり、結果としてメンテナンスが難しいコードが出来上がる。さらに、オブジェクトの直列化の観点からも、モデルの利用を勧める。ページやコンポーネントのフィールドに格納したオブジェクトは、リクエスト毎に直列化され復元される可能性がある。これはパフォーマンスに悪い影響を与えるかもしれない。
(訳注: 生のオブジェクトを渡した場合、それらはまずまちがいなく直列化の対象になるが、モデルであれば、モデルの内包する値が直列化されるかどうかはモデルの実装次第となる。例えば、LoadableDetachableModelは値を直列化せず、リクエスト処理の最初に再ロードする。モデルであれば、さまざまな対応が取れるが、生のオブジェクトを渡すと対応の取りようがなくなる)
コンストラクタ内でモデルを展開するな
コンストラクタ内でモデルを展開すべきではない。つまり、コンストラクタ内でIModel.getObject()を呼ぶべきではないということだ。既に書いたように、ページ・インスタンスは複数のリクエストをまたがって生き続ける。だから、モデルをコンストラクタ内で展開すると、既に廃れた、無駄な情報を保持し続けることになってしまうのだ。onUpdate()やonClick()、onSubmit()といった、ユーザのアクションに対応するイベントハンドラ内でモデルを展開するのならば問題ない(リスト10を参照)。
new Form("register") {
public void onSubmit() {
Registration reg = registrationModel.getObject()
userService.register(reg);
}
}
リスト10
さらにほかにも、isVisible()やisEnabled()をオーバーライドして、その中でモデルを展開するのも問題ない。
拡張元コンポーネントにモデルを引き継げ
モデルは親コンポーネントに引き継ぐべきだ。そうすれば、各リクエストの最後で、確実にIModel.detach()が呼び出される。IModel.detach()の呼び出しは、データのクリーンナップのために必要なのだ。例えば、detach()メソッド内でデータを永続化する独自モデルを実装したとしよう。これはつまり、detach()が呼び出されなければ、データが永続化されなくなってしまうということだ。リスト11は、スーパークラスのコンストラクタへモデルを引き継ぐ例だ。
public class RegistrationInputPanel extends Panel{
public RegistrationInputPanel(String id, IModel<registration> regModel) {
super(id, regModel)
}
}
リスト11
バリデータはデータやモデルを一切更新すべきではない
バリデータは検証のみを行うべきだ。BankFormValidatorを持つ、銀行アカウントフォームを考えて欲しい。このバリデータはウェブサービスから銀行データを取りだし、銀行名が正しいかどうかをチェックする。まさかこのバリデータがデータを更新すると思っている人などいないだろう。そういったロジックはForm.onSubmit()やボタンのイベント処理内で行うべきことだ。
コンストラクタにコンポーネントを渡してはいけない
コンポーネントやページを、別のコンポーネントのコンストラクタに渡してはいけない。
public class SettingsPage extends Page {
public SettingsPage (IModel<Settings> settingsModel, final Webpage backToPage) {
Form<?> form = new Form("form");
form.add(new SubmitButton("changeSettings") {
public void onSubmit() {
setResponsePage(backToPage)
}
});
add(form);
}
}
リスト12
リスト12を見て欲しい。SettingsPageは、サブミットが成功したときに表示すべきページをコンストラクタ引数として受け取っている。この手法は一応動くだろうが、最悪のプログラミングスタイルだし、汎用性も高くない。このコンポーネントをインスタンス化する段階で、ユーザをどのページにリダイレクトするのかが分かってなければならない。インスタンス化に先立ってやらなければならないことがあるということだ。次に表示するページのインスタンス化がビジネスロジックの後になったほうがいいだろう(例えば、HTMLテンプレートに書いた順などだ)。さらに、リスト12の方法では、表示するかどうかは分からないのに、次のページのインスタンスが必要だ。この問題を解決するには、ここでもう一度「ハリウッドの原則」に登場してもらおう(リスト13参照)
public class SettingsPage extends Page {
public SettingsPage (IModel<Settings> settingsModel) {
Form<?> form = new Form("form");
form.add(new SubmitButton("changeSettings") {
public void onSubmit() {
onSettingsChanged()
}
});
add(form);
}
protected void onSettingsChanged() {
}
}
Link<Void> settings = new Link<Void>("settings") {
public void onClick() {
setResponsePage(new SettingsPage(settingsModel) {
@Override
protected void onSettingsChanged() {
setResponsePage(MyPage.this);
}
});
}
}
add(settings);
リスト13
リスト13のコードは前より長くなったが、より汎用的で分かりやすい。このクラスにはonSettingsChanged()というイベントハンドラがあり、このハンドラは更新がうまくいったときに呼ばれることが明確だ。さらに、このコードでは、単に戻り先ページをセットする以外にも、自由に追加の処理を行うことができる。なにかメッセージを表示してもいいし、データを永続化してもいい。
WicketのSessionはグローバルなデータにのみ使え
Wicketでは、セッションオブジェクトは、WicketのベースSessionクラスを拡張した独自のクラスだ。セッションとのやり取りは、型安全だ。Servletのセッションのような、マップを使った構造ではない。Wicketのセッションは、グローバルなデータにのみ使うべきだ。
認証は、グローバルなデータの良い例だ。ログイン情報とユーザ情報は、ほとんどすべてのページで必要となる情報だ。例えばブログアプリケーションでは、ユーザがブログエントリを更新でいるユーザかどうかが分かったほうがいいだろう。それが分かれば、ブログエントリを編集するリンクを表示したり消したりできる。一般的には、認証に関する全ロジックがWicketのセッション・オブジェクト内にあったほうがいい。認証はグローバルなものだし、そこにあるのが自然だ。しかし数ページ程度でだけ使うようなフォーム情報などならば、セッションに入れるべきではない。そういったデータは、各ページのコンストラクタで渡すほうがいい(リスト14参照)。
public class MyPage extends WebPage {
IModel<MyData> myDataModel
public MyPage(IModel<MyData> myDataModel) {
this.myDataModel = myDataModel;
Link<Void> next = new Link<Void>("next") {
public void onClick() {
setResponsePage(new NextPage(myDataModel));
}
}
add(next);
}
}
リスト14
リスト14のように、ページに具体的な情報をまとめて渡してしまうのだ。モデルはフィールドに格納できる。WicketのページはStrutsとは異なり、シングルトンではなく、それぞれが固有のインスタンスだからだ。このWicketの手法には、ユーザが処理を終えたり途中でやめた段階で、データが自動的に消去されるという大きな利点がある。手動で情報を消去する必要はないのだ! この機能が、セッションの疑似的なガーベージ・コレクタの役割をしているのだ。
コンポーネントの生成にファクトリを使わない
ファクトリ・パターンは有用なパターンのひとつだが、Wicketのコンポーネントには合わない。
public class CmsResource {
public Label getCmsLabel(String markupId, final String url) {
IModel<String> fragment = new AbstractReadOnlyModel<String>() {
@Override
public String getObject() {
return loadSomeContent(url);
}
};
Label result = new Label(markupId, fragment);
result.setRenderBodyOnly(true);
result.setEscapeModelStrings(false);
return result;
}
public String loadContent(String url) {
}
}
public class MyPage extends WebPage {
@SpringBean
CmsResource cmsResource;
public MyPage() {
add(cmsFactory.getCmsLabel("id", "http://url.to.load.from"));
}
}
リスト15
リスト15では、CmsFactoryで生成したラベルをページにaddしている。一見これは良さそうに思える。しかし欠点があるのだ。ファクトリを使ったために、継承を活用できないのだ。もはや、onClick()をオーバーライドすることもできない。ファクトリはコンポーネントのインスタンスを生成するSpringサービスかもしれない。この問題に対処するには、CmsLabelのような、新しいラベルを作ることだ。
public class CmsLabel extends Label {
@SpringBean
CmsResource cmsResource;
public CmsLabel(String id, IModel<String> urlModel) {
super(id, urlModel);
IModel<String> fragment = new AbstractReadOnlyModel<String>(){
@Override
public String getObject() {
return cmsResource.loadSomeContent(urlModel.getObject());
}
};
setRenderBodyOnly(true);
setEscapeModelStrings(false);
}
}
public class MyPage extends WebPage {
public MyPage() {
add(new CmsLabel("id", Model.of("http://url.to.load.from")));
}
}
リスト16
リスト16でのラベルの生成は、ファクトリも使っておらず、うまくカプセル化されている。これなら、匿名サブクラス化したり、メソッドをオーバーライドするのも簡単だ。こうなると今度は、「コンポーネント内の(例えばSpringサービスのような)値を初期化するにもファクトリが必要だぞ」という話になってくる。この問題には、IComponentInstantiationListenerの実装を作ることで対処できる。このリスナは各コンポーネントの上位コンストラクタ内で呼ばれる。このインタフェースの実装で一番有名なのが、SpringComponentInjectorだろう。SpringComponentInjectorは、コンポーネント内の @SpringBean アノテーションの付いた全フィールドに、Springビーンをインジェクトする。IComponentInstantiationListenerの独自実装を作るのは簡単なので、もはやファクトリを使う理由はない。IComponentInstantiationListenerについての詳細はJavadocを参照して欲しい。
すべてのページとコンポーネントにはテストを用意せよ
すべてのページとコンポーネントは、テストを備えるべきだ。最も単純なテストは、ただ単にコンポーネントをレンダリングし、技術的な正しさを検証するようなものだ。例えば、子コンポーネントには対となるWicket IDがマークアップ上にあるはずだ。もし、誤字や入力漏れなどで、Wicket IDが正しく定義されてなければ、テストは失敗するはずだ。もっと本格的なテストであれば、フォームの裏にあるコードを実行して、モックを使って検証をすればよい。これでコンポーネントのふるまいが正しいことを検証できる。テストは、プログラミング工程で技術的あるいはビジネス的なバグを発見・修正するための、手軽な方法だ。テスト駆動開発にも、Wicketはよく合うだろう。ページが正しく実装されていることを確認したいなら、まずテストを実行してあげればよい。もしWicket IDが正しく定義されてないバグが含まれていたら、テストが失敗するので、すぐに分かる。ページの確認のために、いちいちサーバを起動する必要はない。サーバの起動には長い時間がかかるので、単体テストには向いてない。Wicketではサーバの起動が必要ないので、開発のサイクルはもっと速く回せる。しかし、AJAXを使ったテストを行うのが難しいという欠点もある。それでも、Wicketのテストサポートは、他のウェブ・フレームワークを上回っている。
他のServlet Filterとのやりとりは避ける
可能なかぎり、Wicketの世界に留まろう。Servlet Filterの利用は避けたほうがいい。Servlet Filterの代わりに、RequestCycleを使い、onBeginRequest()やonEndRequest()をオーバーライドすればよい。HttpSessionについても同じだ。Wicketにはセッションを扱うクラスとしてWebSessionクラスがある。だから、単にWebSessionを拡張したクラスを作り、ApplicationクラスのnewSession()メソッドをオーバーライドすればいい。Servlet仕様の各インタフェースにアクセスする必要は、ユーザ認証のために外部クッキーを読むような例を除けば、ほとんどないはずだ。そういった部分は最小にし、うまくカプセル化して隠蔽しよう。ユーザ認証の例であれば、WicketのSession内に処理をカプセル化するのがいだろう。
小さなクラス、小さなメソッドに分割しよう
単一の巨大なクラスを作らないようにしよう。すべての処理をコンストラクタに入れてしまう開発者はよくいる。そういうクラスは、匿名クラスを幾層にもわたって使っていたりして、とても分かりにくく、ごちゃごちゃしている。論理的なグループにまとめて、正しいビジネス的な名前をつけたメソッドに分解しよう。そうすれば、プログラムのビジネス的な側面がはっきりし、分かりやすくなる。開発者がコンポーネントを見るときは、まずビジネス的側面が知りたいのであって、技術的な側面を見るのはそのあとだ。技術的側面を見る必要が出てくれば、メソッドの実装に潜っていくことになる。実装が疑わしくなってきたときには、コンポーネントをもっと小さな単位に分割すべきだ。小さなコンポーネントは再利用の機会を増やすうえに、テストも簡単になる。リスト17は構造化の例だ。
public class BlogEditPage extends WebPage {
private IModel<Blog> blogModel;
public BlogEditPage(IModel<Blog> blogModel) {
super(new PageParameters());
this.blogModel = blogModel;
add(createBlogEditForm());
}
private Form<Blog> createBlogEditForm() {
Form<Blog> form = newBlogEditForm();
form.add(createHeadlineField());
form.add(createContentField());
form.add(createTagField());
form.add(createViewRightPanel());
form.add(createCommentRightPanel());
form.setOutputMarkupId(true);
return form;
}
}
リスト17
ドキュメントが貧弱だという意見について
Wicketはドキュメントが貧弱だという話はよく聞く。これは一面ではそのとおりだ。一方、コードのひな形として使えるサンプルやコード片がたくさんある。さらに、ややこしい質問にも素早く答えてくれる、大きなコミュニティがある。Wicketでは、ほとんどすべての要素が拡張可能で交換可能なので、Wicketのすべてをドキュメント化するのは、かなり難しいことなのだ。もしあるコンポーネントが、目的にぴったり合わないとしても、コンポーネントを拡張して置き換えられる。Wicketを使うなら、常にコードに潜れということなのだ。たとえば、バリデータを考えてみよう。バリデータが今どれだけあるのか知るにはどうしたらいいだろう。IValidatorインタフェースを開き(EclipseならCtrl+Shift+Tだ)、続けて型階層を表示しよう(EclipseならCtrl+Tだ)。Wicketと、自分のプロジェクトにある、すべてのバリデータが確認できるはずだ。
まとめ
この記事は、Wicketでよりよい、メンテナンスしやすいコードを書くために役立つよう、まとめたものだ。ここに書いていることはどれも、何回かのWicketを使ったプロジェクトによって実証されたものばかりだ。この記事が、あなたのWicketプロジェクトの未来に役立つことを望む。
リンク