マクロを便利に使う
この記事はもともとTumblrに書いていた自分のブログ記事を転載したものです。投稿日時も当時の投稿日時を再現してあります。
マクロは自分で作るもんなのか?
Clojureでばりばり書いている人からすると愚問で、「Yes」としか言いようがない質問だろうけども、前に、とある場所でClojureについてのLTをしたときに、「マクロとかagentとかは実際に使うもんなんですかね?」と質問を受けたことがあります。
『フレームワークで使うテクニックなんかはフレームワークを「使う側」が知る必要はない』みたいな話の延長なのか、あるいは、そもそも現場での使いどころがイメージできない、ってことなのか、つかみ損ねましたが、実例があると分かりやすいのかなあと思いまして、実際に自分はどういう風に使っているのか、ここに書いておこうと思いました。
文法をつくれ!
やっぱり、実際に使う一番多い用途はこれだとおもうんですよね。新しい文法を作る。
とはいえ、ほとんどのClojureのテキストブックには、マクロに対するClojureの立場みたいなもんが書いてるはずです。つまり「必要ないならマクロを使うな!」と。
同じ書き方で関数でできることを、わざとマクロでやるのはいいかっこしいなんですよ。関数で出来ないからマクロでやる、であるべき、ということでしょう。
(まあ、マクロはデバッグもしにくいですし)
でも、こう書きたいけど関数ではできない、というケースは、思ったよりたくさんあります。関数で書いてみて、関数ではできないとわかったら、マクロでやればいいのです。
誰でも「ああそういうケースありそう」と思える実例として、次のようなケースを考えてみましょう。
値がnilでない時のみ、その値をマップに入れる
私が実際に使っている例として、ある値がnilでないなら、その値をマップに格納したい、という用途があります。
引数がnilだったらマップには入れたくない、とかいろいろ状況は考えられますが、単純な例として「マップ同士の移し替え」を考えてみましょう。
とりあえず、以下のようなデータがあるとします。便宜上Clojureのマップとして表現します。各キーは、存在したりしなかったりします。
{:book_name "本の名前" :isbn_code "ISBNコード" :publish_date "発行日" :publisher_name "発行会社"}
これを、次のようなマップに移し替えます。
{:type :book :name "本の名前" :isbn "ISBNコード" :published "発行日" :publisher "発行会社"}
:typeという独自キーを持っているのと、それぞれ微妙にキー名が異なるわけです。もしもとのマップに :book_name しか入っていなければ、次のようなマップを得たい。
{:type :book :name "本の名前"}
変換後も、元のマップにないキーは存在しないわけです。単純な詰め替え処理を書くと、次のようなマップが得られてしまうはずです。
{:type :book :name "本の名前" :isbn nil :published nil :publisher nil}
assocするとキーは生成されてしまうからです。すぐ使い捨てるデータならこれでもいいのですが、nilだったらキー自体が無くなって欲しいということもあるでしょう。
普通に関数で書くと、以下のような冗長なコードになります。もとのマップはoriginalというシンボルでアクセスできるものとします。
(cond-> {:type :book} (:book_name original) (assoc :name (:book_name original) (:isbn_code original) (assoc :isbn (:isbn_code original) (:publish_date original) (assoc :published (:publish_date original) (:publisher_name original) (assoc :publisher (:publisher_name original))
cond-> 命令は、左の式が真だったら、右の式を実行します。右の式の第1引数には、直前の式の結果が渡されます。初期値は :type のみをキーにもつマップで、最初の判定が真だったらこれが最初の式の第1引数に渡り、次も真だったら最初の式の結果が2番目の式の第1引数に渡ります。すべて真だったら、4回 assoc が実行されるわけです。
同じコードが4回並んでいることがわかります。いかにもめんどくさい。4つならいいけど、フィールドが20個とかあるとしんどい。
で、もやもやと「こういうふうに書きたいなあ」と思ったとします。
(assoc-> {:type :book} :name (:book_name original) :isbn (:isbn_code original) :published (:publish_date original) :publisher (:publisher_name original))
右側の式が真だったら、右側の式の結果を、左側の値をキーにしてassocすることを繰り返すわけです。
「こうかけたらいいな」と思ったのなら、そう書けるようにすればいいわけです。元のコードがあり、「こう書きたい」というコードがあるなら、元のコードを、「こう書きたい」というコードに変換すればいいのです。コンパイル前にコードを書き換える機能、それがマクロです。
復習: マクロとはなにか
前述したようにマクロとは、コンパイル前に、ある式を別の式に変換する機構です。コンパイラは、変換後のコードをコンパイルします。
マクロとはClojureの関数で、引数は、Clojureのコードそのものです。また、結果としてClojureのコードを返します。ここで役に立つのが、Clojure(というかすべてのLISP)の特徴である、「プログラムコード自体がClojureのデータである」という性質です。
マクロには、引数としてClojureのコードそのものが渡ってきますが、これはソースという文字列の塊ではなく、「シンボルのリスト」として渡ってきます。たとえば、
(when (> age 19) (println "OK"))
whenはマクロです。whenマクロには、第1引数として(> age 19)というリストが、第2引数として、(println "OK")というリストが渡されます。
それぞれはリストですので、Clojureのあらゆるリスト操作関数が使えます。第1引数に対して first を使えば、「>」というシンボルを得られます。secondなら「age」が得られます。
引数にリストとして渡ってきたコードを、マクロ内で再構成し、新しいコードを表すリストを返せば、コンパイラはそれをコンパイルするのです。
つまり、前述の例であれば、「こうかければいいな」とモヤモヤ考えていたassoc->というマクロを書き、マクロ内で、実際に動作する、めんどくさいcond->を使った式に書き換えて返せばいいわけです。
別の関数を使って書けることであれば、全部マクロで書けます。その「書けることを分かっている」式に変換すればいいのですから。
Clojureには、マクロを書きやすくするために、マクロテンプレートとして使える「構文クオート」という仕組みがあります。「`」(バッククオート)で式を始めれば、それ以降はマクロテンプレートになります。マクロテンプレートはマクロが返す値であり、テンプレート内には、「~」あるいは「~@」を使って、マクロ外から受け取った値を埋め込めます。
assoc->マクロを作る
構文クオートを使うと、assoc->マクロは次のように書けます。
(追記: convert-clases関数は、あとから分配束縛を使えばもっと簡単に書けることに気がついたので、最初にポストしたあとに今のものに書き換えました)
(defn- convert-clauses [[k v & clauses :as data]] (when (seq data) (concat [v (list 'clojure.core/assoc k v)] (convert-clauses clauses)))) (defmacro assoc-> [rec & clauses] (let [converted (convert-clauses clauses)] `(cond-> ~rec ~@converted)))
assoc->マクロの第2引数以降のすべての式は、clausesに入ります。convert-clausesは、clausesリストを引数に、
[(:book_name o) (clojure.core/assoc :key1 (:book_name o) (:isbn_code o) (clojure.core/assoc :key2 (:isbn_code o)]
というベクタを作り出します。convert-clausesの中で、Clojureソースコード自体を分配束縛(destructuring)でk, v, clausesというシンボルに分解したり、concatなどを使って操作していることが分かると思います。
assoc->マクロは~@を使って、このベクタを展開してテンプレート内に貼り付けます。これで晴れて、
(assoc-> {} :key1 (:book_name o) :key2 (:isbn_code o))
は、
(cond-> {} (:book_name o) (clojure.core/assoc :key1 (:book_name o) (:isbn_code o) (clojure.core/assoc :key2 (:isbn_code o))
といったcond->式に変換されます。
まとめ
Clojureのマクロは、Clojureのリストを引数に受け取って、Clojureの関数でコードを操作することができる仕組みです。
文字列を操作するのであれば、パース処理などが必要で大変ですが、Clojureであれば、「シンボルのリスト」をClojureで操作するだけで、コードを変換することが出来ます。
もし関数で書いてもうまく理想の書き方ができないなら、マクロでやってみればいいと思います。
Clojureのprotocol実践
この記事はもともとTumblrに書いていた自分のブログ記事を転載したものです。投稿日時も当時の投稿日時を再現してあります。
自社開発ではClojureを中心に使っていて、ユーザーが増えて欲しい気持ちもあって、Clojureのテキスト的なものを書いていては挫折、などを繰り返してました。キリがないので、テキストのことは気長に構えて、開発中に気がついたことはどんどんブログに書いて行こうと思います。
手始めに、最近やっと「実用的」という意味で使えるようになったかなーと思えた、「プロトコル」について。
Clojureのプロトコルは、JavaでいうところのInterfaceのようなものです。型名称と関数定義だけを束ねたものです。defprotocolによってプロトコルを定義することができます。
Javaプログラマ歴が長いので「まあInterfaceなら余裕だな」と構えていましたが、意外と使い方が難しい。以下、Javaプログラマ的感覚でプロトコルを使うと使いにくくなりかねない点について書いてみます。
「標準実装」という概念はない
Javaの場合、すべてのプログラムはなんらかのクラスの中に書きますし、さらにクラスの継承関係、特に実装継承というものが当たり前に存在します。もう、空気のようなもんですからInterfaceを用意する場合も、そのInterfaceの標準実装を、クラスとして提供することを前提にしていたりします。DefaultXXXとかAbstractXXXとかそんなのですね。だから、次のようなInterfaceを定義しがちです。
public interface IComponent { boolean isVisible(); boolean isEnabled(); List<Node> makeNodes(Session session); List<String> getDependentCssFiles(); List<String> getDependentJavaScriptFiles(); }
isVisibleとisEnabledはほとんどのケースでは常に真であり、getDependentCssFilesとgetDependentJavaScriptFilesとは、デフォルトでは空のリストを返す、なんてことを空気のように想定していたりします。「デフォルトでは」と。
「デフォルト」がない!
これは実際に私がやらかしたことです。
前提として、私はClojureのような関数型言語であっても、データとその操作をする関数を束ねておく、という考え方はコードの書き方として重要だと思っています。「データとその操作」というコード整理の仕方はオブジェクト指向が普及するよりも前にすでに実践されていたわけだし(クラシックMacのAPIなんかは勉強になります)、なにより、どの関数が何をやっているのかが分かりやすい。ClojureはJavaと比べるとクラスに縛られないので、もう少し緩くコードをかけますが(クラスに結びつけられないものを無理矢理クラスにする、ということはせずに済む)、大枠としてはそういう構造を守りたいな、と思ってました。
ところが、人間は弱いもので、安直な書き方ばかりしてきてしまい、実際のコードとなると、各関数が操作するデータはまちまち、ひとつひとつ思い出さないとコードが読めない、という事態になってしまいました。
これはやばいということで、本気でちゃんと整理しようと思ったのが発端です。
Clojureにはデータ構造と操作を束ねる機構として、関数の集合を作るdefprotocol、データの集合とプロトコルを結びつけるdeftypeとdefrecordというマクロが用意されていて、これらを使えば、Cのころのように、頭を振り絞ってライブラリ構造を整理することもなく、すんなりと似たような構造を実現できそうです。それらのデータタイプが実装している関数群を示すのが、プロトコルです。
プロトコルは別にrecordとtypeに限定されたものではなく、Clojure(とJava)のあらゆるデータ構造は、プロトコルを「実装」できますが、ここでは関係ない話題なので、興味ある人はextend-protocolとか調べればいいかと。
私はWeb向けのアプリケーションを書いているので、まさに上記のようなComponentの概念が必要になりまして、次のような感じのプロトコルを定義したのです。このプロトコルを、ダイアログとか、画面上の部品なんかで使えばいいだろう、という感覚です。Java脳です。
(defprotocol Component (css-files [this]) (js-files [this]) (visible? [this]) (enabled? [this]) (make-nodes [this session]))
ところが、うまく行きませんでした。前にも書いたように、css-files, js-files, visible?, enabled? には想定しているデフォルト動作があって、ほとんどのケースではそのデフォルト動作で済むのです。しかし、ClojureはMLなんかをみても、実装と型は明確に分離されているべき、実装は継承すべきではない、という考え方が強い言語でして、このプロトコルに「標準実装」を提供する方法がありません。毎回、同じような実装を書かないといけない。
(defrecord LoginDialog [] Component (visible? [this] true) (enabled? [this] true) (css-files [this] nil) (js-files [this] nil) (make-nodes [this session] (create-login-dialog-nodes session))
この例であれば、visible?からjs-filesの4行はほとんどのケースで同じになります。毎回同じです。
実際には、「Clojure Programming」なんかを読むと「extend使って、実装を、キーが関数名、値が関数であるようなマップで提供すれば、『デフォルト実装関数マップ』を用意しておくだけで、なんと!merge関数一発でデフォルト実装を『継承』できてしまう!すごい」って誇らしげに書いてるのですが、正直コードがやぼったくなるし遅くもなるので、あんまりやりたくない。
プロトコルは小さくする
でいろいろどうすればプロトコルをいい感じに使えるのかなーと、いろんなClojureライブラリを読んでいたのですが、ふと、どのプロトコルもとても小さいことに気がつきました。プロトコルの関数は2個とか1個だったりするケースが多い。
ああ、そうか。プロトコルは小さくていいんだ。と当たり前のことに気がつきました。
小さなプロトコルをたくさん定義して、そのプロトコルについて、デフォルトとして想定している動作でない動作を提供したい時のみ、プロトコルを実装すればいい。あるデータ型がプロトコルを実装してないなら、デフォルトの動作をすればいい。
というわけで、次のような細かいプロトコルに分割しました。
(defprotocol Visible (visible? [this])) (defprotocol Enabled (enabled? [this])) (defprotocol CssFileProvider (css-files [this])) (defprotocol JavaScriptFileProvider (js-files [this])) (defprotocol NodeProvider (make-nodes [this session]))
常に実装すべき関数はmake-nodesだけなので、それについてだけは、汎用関数を用意しておきます。
(defn make-base-nodes [c session] (when (instance? Visible c) (when-not (visible? c) []) (when (instance? Enabled c) ... (when ... (when ...
みたいな感じで、ここで「もしプロトコルAを実装してれば…」みたいな判定を行い、実装してなければデフォルトの動作をすればよい。
こうすれば、実際に型定義をするときは、必要なプロトコルだけを実装すればいい。
(defrecord LoginDialog [] NodeProvider (make-nodes [this session] (create-login-dialog-nodes session)))
シンプル。
同じ方式で、プログラムの大部分を改修したら、かなりプログラムの見通しがよくなりました。
まとめ
実装継承のような仕組みがない世界では、大きなプロトコルは避けないと、同じコードを毎回書くことになってしまいます。プロトコルは小さくまとめて、必要な時だけ実装するのが良い、というのは、ちょっとした発想の転換でした。
追記
コメントで、マルチメソッドを使えばいいって話が出ました。たしかに、マルチメソッドはデフォルト実装も持てますし、形象関係の定義すらできるので、よさそうですね。どうも私はマルチメソッドは「便利なcond」としてしか使ってないみたい…
プロトコルには、「関数を束ねて管理する」という利点があると思いますが、今回紹介したケースでは、すべてを関数ひとつだけのプロトコルに分割してしまいましたから、マルチメソッドでやればいいように感じます。
マルチメソッドは仕組み上ちょっと遅いようですが、そこまで速度が要求されるところでもないですし。
Apache Wicket ベストプラクティス(日本語)
この翻訳記事は、もともと私の個人サーバ上に設置していたブログで公開していたものです。個人サーバを撤去した関係で一時Tumblrへと転載していましたが、このたび、はてなブログへ再転載しました。投稿日付はTumblrへ転載した際の日時です。Tumblrに載せているものはいずれ削除する予定です。
Carsten Hufe氏がブログにて公開している「Apache Wicket - Best Practices (English)」の翻訳許可を御本人から頂きましたので、日本語化しました。Apache Wicketを使っていく上で、とても重要なことがまとめられている、Wicket開発者必読の記事です。特に「常にモデルを使え」というプラクティスは、Wicketのパワーをちゃんと生かすためにには必須のプラクティスです。
ぜひ一読して、今後の開発に生かしてください。
- オリジナル版著者: Carsten Hufe
- オリジナル版URL: http://www.devproof.org/wicket_best_practice
- 翻訳: 矢野 勉
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する必要がある。
// Poor component public class RegistrationForm extends Form<Registration> { public RegistrationForm(String id, IModel<Registration> regModel) { super(id, new CompoundPropertyModel<Registration>(regModel)) // Wrong: RegistrationForm provides its own components 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() { // do something } }); add(form); } }
<html> <body> <form wicket:id="form"> <!-- These are internal structure information from RegistrationForm --> Username <input type="text" wicket:id="username"/> First name <input type="text" wicket:id="firstname"/> Last name <input type="text" wicket:id="lastname"/> <!-- Above new components from page which the user knows --> <input type="submit" wicket:id="register" value="Register"/> </form> </body> </html>
リスト2
リスト2は、RegistrationPageで実際にこの出来の悪いコンポーネントを使ってみた例だ。入力フィールド firstname、lastname、username に注目して欲しい。これらのHTMLフィールドに対応するコンポーネントは、RegistrationForm内でaddされているので、RegistrationPageではaddされていない。このような、他の開発者がコンポーネントがどこでRegistrationPageにaddされたのか分からなくなるようなコンポーネントの作り方は避けよう。
// Good component 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); // Correct: Add components to Form over the instance variable 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() { // do something } }); 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を参照)。コンポーネントツリーの構築が条件によって異なると、同じインスタンスの再利用が大きく妨げられてしまう。
// typical struts 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を使った例を見てみよう。このパネルは、ユーザがまだログインしていない時だけ表示される。
// Poor implementation 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 { // constructor etc @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() { // hide headline when it contains Berlosconi String headline = getModelObject(); setVisibilityAllowed(headline.startWith("Berlosconi")) } }
リスト8
Note: この節は2011年10月15日に書き替えた。可視性はonConfigure()内でsetVisibilityAllowed(boolean)を使って設定すべきで、isVisible()をオーバーライドすべきではない。読者のフィードバックを受けて改訂した。ありがとう。
常にモデルを使え
常にモデルを使え! 生のオブジェクトを直接コンポーネントに渡すべきではない。ページやコンポーネントのインスタンスは複数のリクエストにまたがって生き続けるが、ここに生のオブジェクトを使っていたら、後からそのオブジェクトを別のものに置き換えることができなくなる。LoadableDetachableModelによってリクエスト毎にロードされるエンティティを考えてみて欲しい。もしエンティティを直接コンポーネントに渡していると、エンティティ・マネージャが新しいオブジェクト参照を作り出しても、コンポーネントは廃れたオブジェクト参照をキープし続けることになる。コンポーネントのコンストラクタ引数には、常に IModel
public class RegistrationInputPanel extends Panel{ // Correct: The class Registration gets wrapped by IModel public RegistrationInputPanel(String id, IModel<Registration> regModel) { // add components } }
リスト9
リスト9の方法であれば、コンストラクタはモデルのどんな実装でも受け取ることができる。Modelクラスはもちろん、PropertyModelであろうと、値を自動的にロードして永続化するLoadableDetachableModelの独自実装であろうと、なんでもだ。モデル実装は容易に置き換え可能だ。モデルのユーザとして知っておくべきことは、IModel
(訳注: 生のオブジェクトを渡した場合、それらはまずまちがいなく直列化の対象になるが、モデルであれば、モデルの内包する値が直列化されるかどうかはモデルの実装次第となる。例えば、LoadableDetachableModelは値を直列化せず、リクエスト処理の最初に再ロードする。モデルであれば、さまざまな対応が取れるが、生のオブジェクトを渡すと対応の取りようがなくなる)
コンストラクタ内でモデルを展開するな
コンストラクタ内でモデルを展開すべきではない。つまり、コンストラクタ内でIModel.getObject()を呼ぶべきではないということだ。既に書いたように、ページ・インスタンスは複数のリクエストをまたがって生き続ける。だから、モデルをコンストラクタ内で展開すると、既に廃れた、無駄な情報を保持し続けることになってしまうのだ。onUpdate()やonClick()、onSubmit()といった、ユーザのアクションに対応するイベントハンドラ内でモデルを展開するのならば問題ない(リスト10を参照)。
new Form("register") { public void onSubmit() { // correct, unpack model in an event call 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) // add components } }
リスト11
バリデータはデータやモデルを一切更新すべきではない
バリデータは検証のみを行うべきだ。BankFormValidatorを持つ、銀行アカウントフォームを考えて欲しい。このバリデータはウェブサービスから銀行データを取りだし、銀行名が正しいかどうかをチェックする。まさかこのバリデータがデータを更新すると思っている人などいないだろう。そういったロジックはForm.onSubmit()やボタンのイベント処理内で行うべきことだ。
コンストラクタにコンポーネントを渡してはいけない
コンポーネントやページを、別のコンポーネントのコンストラクタに渡してはいけない。
// Bad solution public class SettingsPage extends Page { public SettingsPage (IModel<Settings> settingsModel, final Webpage backToPage) { Form<?> form = new Form("form"); // add components form.add(new SubmitButton("changeSettings") { public void onSubmit() { // do something setResponsePage(backToPage) } }); add(form); } }
リスト12
リスト12を見て欲しい。SettingsPageは、サブミットが成功したときに表示すべきページをコンストラクタ引数として受け取っている。この手法は一応動くだろうが、最悪のプログラミングスタイルだし、汎用性も高くない。このコンポーネントをインスタンス化する段階で、ユーザをどのページにリダイレクトするのかが分かってなければならない。インスタンス化に先立ってやらなければならないことがあるということだ。次に表示するページのインスタンス化がビジネスロジックの後になったほうがいいだろう(例えば、HTMLテンプレートに書いた順などだ)。さらに、リスト12の方法では、表示するかどうかは分からないのに、次のページのインスタンスが必要だ。この問題を解決するには、ここでもう一度「ハリウッドの原則」に登場してもらおう(リスト13参照)
// Good solution public class SettingsPage extends Page { public SettingsPage (IModel<Settings> settingsModel) { Form<?> form = new Form("form"); // add components form.add(new SubmitButton("changeSettings") { public void onSubmit() { // do something // e.g. persist data onSettingsChanged() } }); add(form); } // Hook protected void onSettingsChanged() { } } // The usage of the new component Link<Void> settings = new Link<Void>("settings") { public void onClick() { setResponsePage(new SettingsPage(settingsModel) { @Override protected void onSettingsChanged() { // Referenz der aktuellen Seite 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() { // do something 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) { // load some content } } // create the component within the page: 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); } } // create the component within a page 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; } // more methods here }
リスト17
ドキュメントが貧弱だという意見について
Wicketはドキュメントが貧弱だという話はよく聞く。これは一面ではそのとおりだ。一方、コードのひな形として使えるサンプルやコード片がたくさんある。さらに、ややこしい質問にも素早く答えてくれる、大きなコミュニティがある。Wicketでは、ほとんどすべての要素が拡張可能で交換可能なので、Wicketのすべてをドキュメント化するのは、かなり難しいことなのだ。もしあるコンポーネントが、目的にぴったり合わないとしても、コンポーネントを拡張して置き換えられる。Wicketを使うなら、常にコードに潜れということなのだ。たとえば、バリデータを考えてみよう。バリデータが今どれだけあるのか知るにはどうしたらいいだろう。IValidatorインタフェースを開き(EclipseならCtrl+Shift+Tだ)、続けて型階層を表示しよう(EclipseならCtrl+Tだ)。Wicketと、自分のプロジェクトにある、すべてのバリデータが確認できるはずだ。
まとめ
この記事は、Wicketでよりよい、メンテナンスしやすいコードを書くために役立つよう、まとめたものだ。ここに書いていることはどれも、何回かのWicketを使ったプロジェクトによって実証されたものばかりだ。この記事が、あなたのWicketプロジェクトの未来に役立つことを望む。
リンク
- http://wicket.apache.org/ (Apache Wicket)
- http://wicketstuff.org/wicket14/ (Wicket Examples)
- http://en.wikipedia.org/wiki/Hollywood_Principle
Daniel Bartlの指摘に感謝します