Atlassian BambooでCI環境を構築する
この記事はもともとTumblrに書いていた自分のブログ記事を転載したものです。投稿日時も当時の投稿日時を再現してあります。
やらないといけないと思いながらも、ずぼらでずっと後回しにしてた、CIサーバ構築作業を最近やりました。というのも、JIRAを契約するついでにお試しに登録していた、Atlassian Bambooのお試し期間が過ぎてしまったから。お試しすらしていない…
とりあえず解約するにしても試してからにしようってことで本腰入れて使ってみることにしました。でも、ネットで検索してもCircleCIいいよってのが多く、Bambooは「どう設定していいのかぱっとみわからなかった」みたいなのが多い印象。
たしかに、ぱっと見どう使っていいのかなんか分かりにくい…
でも、分かってしまえば仕組みは簡単でした。
Bambooって結局なんなのか
Amazon EC2を実行サーバとして利用する、CI環境です。Bamboo上にソースリポジトリと「Builder」や「Tester」の設定をしておくと、ソースをリポジトリにpushすると、Bambooが指定したEC2インスタンスを自動起動して、ソースチェックアウトからビルド&テストしてくれます。EC2インスタンスは自動停止します(これについては後述)
ポイントは
- AmazonのAMIを指定するとそのサーバを起動して使うので、ビルド&テスト環境を自由に構築できる(自分でEC2上に環境構築して、そのインスタンスからイメージをつくればいいので)。自由度は高い
- Bambooのコストとは別にEC2のコストがかかる
- ビルトインで提供されているBuilderとかを使える状況であれば、出来合いのAmazonインスタンスを使えば即使える
って感じですか。 私はClojure+Leiningen+Midjeという環境が必要なので、サーバ設定を自前でできるのはありがたい。使ってみようという気が湧きました。
ただ、ドキュメントが猛烈に分かりにくいように思います。何ヶ所かつまって、検索すると、Atlassian自身のフォーラムに同じ質問が(英語で)上がってて解決したり、みたいなことがけっこうありました。
ワナ
私が一日無駄にしたワナが、Bamboo OnDemand(ダウンロード版)では、Atlassian提供のAmazon EC2イメージを使ってサーバ構築しないと、Bambooが接続するためのエージェントが起動しないってのがよくわかってなくて、それが分からなかったのが
tokyoリージョンにはイメージが提供されてない
ってことがわからなかったからです。なんとかビルドできる環境作っても、エージェント起動開始でずっとPending…ってなって困ってたのですが、そもそもエージェント・プログラムがサーバに入ってないのだからあたりまえです。
でもねえ、「Elastic Bamboo Global Settings」ってところに、いきなりリージョン選べってのがあるんですよ。そりゃTokyo選びませんか?
Global Settings画面
Bamboo OnDemandを使う場合は、ここは(ちゃんと動く環境ができるまでは)US East (Northern Virginia)固定でOK。デフォルトがたぶんそうなってるので、かえなきゃいいわけです。
すべて動くようになってから、EC2のイメージを(EC2の機能で)Tokyoリージョンにコピーして、Bamboo側もTokyoリージョンに変更、ということは可能ですが、Bambooインスタンスに接続するのはBamboo自身であって、自分でも顧客でもないのだから、インスタンスがTokyoで起動する意味もないわけで、ずっとUS Eastでもいいはず。
US Eastであれば、Bambooの「Image Configurations」画面に、すぐに使用可能なイメージがずらっと表示されます。Tokyoに変えていると、ここが空っぽです(空っぽなので、ほんとはここに既成イメージが表示されるなんてことが想像できない)
Elastic Bambooの設定画面でAWS access key idを設定済みであれば、Startボタンを押すと、EC2でインスタンスが起動します。あとはEC2インスタンスにSSHでログインして、ビルド環境を構築した後、EC2側でインスタンスからイメージを作成し、そのイメージを、Image Configurationsに追加してやれば、独自のビルドサーバが作成できます。
ただ、Java+Maven+JUnitとかRubyとかPHPとかは、実はビルドやテストを実行できるだけの設定がすでに既成イメージにされているので、上記から適当にひとつ選んでもいいかもしれません。ただLargeイメージを使う意味がない場合は、既成イメージのAMI idを使って、インスタンスタイプだけをMicroに変更したような定義を一個作った方が、財布に優しいと思います。
ともかく、US Eastリージョンで既成イメージをベースに環境構築する(あるいはそのまま使う)ってことだけ忘れなければ、そんなにはまることなくさっさと動かし始められると思います。ただEC2につないでるだけですから…
EC2インスタンスの自動停止
Bambooの設定に、リモートサーバのエージェントが何分以上アイドルになったらインスタンスを止めるかって設定があって、デフォルトが10分なんですが、10分経っても落ちません。実は、サーバの自動停止にはもうひとつ条件があって、なぜかドキュメントに書いてなくて、AtlassianフォーラムのQ&Aで見つけたのですが…
- EC2の支払いが最小になるように、EC2の最小課金時間(通常は1時間?)の間は起動し続ける
ということのようです。
EC2のインスタンスは、起動サーバ単位で1時間いくら、なんですが、この1時間というのは「1時間未満はすべて1時間とみなす」という条件がついてます。よって、1分で停止しても1時間分取られます。
Bambooは、ソースがプッシュされるたびに毎度サーバを起動&停止するのではなく、1時間以内であれば、同じインスタンスを使い回そうとします。アイドル時間が10分たっても、1時間以内にまた別のビルド要求が来るかもしれないので、ギリギリまでは起動し続けます。そのまま要求がこないと、最小課金時間が過ぎるちょっと前に(私が見てた限りでは55分くらいで)自動停止します。 なかなかがんばってるなーと思いました。
Capabilityの設定
Bambooに設定できる各種コマンドは「Capability」と呼んで、インスタンス設定画面の既成インスタンス一覧にある「View Capabilities」を押すと、そのイメージにあるコマンド等を設定できます。
Ant, Grails, Maven, Node.js, PHPUnit, JDK, Mercurial, Gitは始めから入っています。あと、設定画面には出ていないけど、サーバにログインしてみると、/optの下に既にJDK8やMaven 3.2が入ってたりもするので、それらは、サーバをいじらずに、Capabilityに定義を追加するだけで、使い始められます。
私はClojureソースをビルドするためにLeiningenをサーバに入れたので、次のように定義を足しました。
このようにCapabilityを足すと、この後のタスク設定なんかで、「Leiningen」を選択できるわけです。
ステージとジョブ、タスク
ビルドプランの設定はまあ直感的だと思います。 ステージ・ジョブ・タスクの区別が分かりにくいかなーと思いました。
- ひとつのプランは複数のステージを持てる
- ステージは順番に実行される
- ひとつのステージは複数のジョブを持てる
- ジョブは 並列に 実行される
- ジョブは複数のタスクを持てる。
- タスクは順番に実行される
という構造で、ジョブが並列に実行されるってことだけ分かってればいいかと。
以下の画像では、
- 「Build Project」という名前のステージが
- 「Build Source」というひとつのジョブを持っていて、そのジョブが
- 「Source Code Checkout」「Maven 3.x」「Command」という3つのタスクを持っている
という構造です。
タスクは、Bambooにもともといくつか用意されていて(「Source Code Checkout」とか)、それらを並べるだけでいいです。サーバイメージに自分でインストールしたコマンドについては、「Command」タスクを使って、Executableを指定できます。
こんな感じです。先にCapabilityを設定しているので、コンボボックスでLeiningenを選択できるようになっています。
上記イメージのように、JVM系コマンドの場合、JAVA_HOMEとPATHをせっていしなくちゃいけません。Bambooエージェント自体はJava 6で動いている(サーバ設定いじると変えられますが、エージェントが起動しなくなります)ので、タスクの設定で、使用したいJVMを指定してください。 上記画像ではJava 8を設定しています。ちなみに、Java 8は既成イメージの始めから入ってました(なぜかCapabilityには入ってないので、自分で足しましたが)
ビルドしたいソースが、チェックアウトしたソースのサブディレクトリの場合は、「Working sub directory」にリポジトリトップからの相対パスを書きます。私の場合、javaソースは「java」というサブディレクトリに、Clojureソースは「clojure」というサブディレクトリに入れているので、「clojure」を指定しています。
言葉で書くと長ったらしいですが、設定自体はさくさく終わります。
あとはビルドプランを一発手動実行して、動くようならOK。リポジトリにソースがプッシュされたら、勝手にビルドが走ります。
Bambooについて日本語で書いてるブログがほとんど見つからなかったので、書いてみました。
シェルスクリプトでtmuxのウインドウをまとめて開く
この記事はもともとTumblrに書いていた自分のブログ記事を転載したものです。投稿日時も当時の投稿日時を再現してあります。
開発中、常に5つくらいのウインドウをtmuxで開いて作業しています。開くウインドウは毎回同じなのですが、毎回毎回5つウインドウを作って、リネームして、というのが面倒です。
たぶんコマンドから制御できるんだろうと思い調べたらやっぱりあったので、メモとして書いておきます。
DIR="/path/to/your/development/dir" tmux new-session -d -s myproj -n project1 -c "$DIR/project1" tmux new-window -n project2 -c "$DIR/project2" tmux new-window -n project3 -c "$DIR/project3" tmux new-window -n project4 -c "$DIR/project4" tmux new-window -n project5 -c "$DIR/project5" tmux attach -t myproj
これで、tmux.shを起動したら5つのウインドウがセッティングされた状態でtmuxが開きます。
スレッドマクロを整理する
この記事はもともとTumblrに書いていた自分のブログ記事を転載したものです。投稿日時も当時の投稿日時を再現してあります。
スレッドマクロって?
スレッドマクロ(threading macro)は、Clojureのソースを人間に読みやすい形で書けるマクロで、現在のClojure 1.5.1には、結構な数のが用意されています。1.5で初めて追加されたものもありますし、まとめておくと、今後Clojureを始める人にも役立つかもしれないなあってことで、ブログ記事に書いておくことにしました。1.5で導入された新しいスレッドマクロも含めて既に知っている人には役に立たないのであしからず。
「スレッド」マクロといっても、並列プログラミングのスレッドとはまったく関係がないです。Clojureの -> や ->> のような、矢印系マクロの総称として使われています。Clojure 1.5.1では、次のスレッドマクロがあります。
- ->
- ->>
- as->
- some->
- some->>
- cond->
- cond->>
基本は -> と ->> で、他のものは、この基本スレッドマクロの便利版と思ってよいです。
スレッドマクロの役割は、関数ベースのClojureでは「実行する順」に書けない、もしくは書いても冗長になるコードを、簡便に、「実行する順」に書けるようにすることです。「AしてBしてCする」みたいな処理を簡単に書けるようにするわけです。
(func3 (func2 (func1 :arg) :arg) :arg)
これを、
(-> (func1 :arg) (func2 :arg) (func3 :arg))
と書けるようにします。
-> (thread-first) マクロ
-> はその性質から「thread-first macro」と呼ばれるようです。->マクロは、初期値と、スレッディング対象となる式を受け取ります。実物を見た方がわかりやすいです。
(-> {:type :person :age 30} (assoc :name "John Doe") (assoc :city "tokyo") (dissoc :age))
この例では、初期値は {:type :person, :age 30}
というマップです。この初期値が、次のフォームである (assoc :name "John Doe")
の第1引数の位置に挿入されます。
続けてこの (assoc 初期値 :name "John Doe")
が、2つ目のフォームである (assoc :city "tokyo")
の第1引数になります。さらにこの assoc
が、最後のフォームである (dissoc :age)
の第1引数になるのです。
(assoc
を2回呼ぶなら可変長引数でひとつにまとめろ、という話はちょっと脇へ置いておいてください)
式が挿入される位置を x
で表現してみると、次のように、
(assoc x :name "John Doe") (assoc x :city "tokyo") (dissoc x :age)
式が挿入される位置に書いた x
が、各フォームを貫いているように見えます。
このように、前の引数を次のフォームの同じ位置で使う場合に、読みやすいコードを書けるわけです。
もしスレッドマクロがなければ、前述のコードは、次のような関数の入れ子として書かなければいけません。
(dissoc (assoc (assoc {:type :person, :age 30} :name "John Doe") :city "tokyo") :age)
とても読みにくいです。無理矢理でも実行順に書こうとすると、let
を使って各結果値をシンボルに束縛しなければならないでしょう。
(let [init {:type :person, :age 30} x (assoc init :name "John Doe") y (assoc x :city "tokyo")] (dissoc y :age))
let
の各シンボルは使い捨てみたいで、なんだか疲れます。スレッドマクロであれば、直前の結果を次の式ですぐに使う、というコードを、簡潔に記述できます。
-> はスレッドマクロの基本であり、ほかのものは、「-> とどこが違うのか」で説明できます。
->> (thread-last) マクロ
->>マクロと->マクロの違いは、引数が入る位置が、第1引数ではなく、最終引数の位置になる、という点だけです。Clojureの関数の多くは、関数の操作対象を第1引数で受け取るように作られているのですが、第1引数として関数を取る関数の多くは、操作対象を最終引数で受け取ります。map, filter, distinctといった、コレクション操作系の関数のほとんどが、そのような作りになっています。
->>マクロを使えば、コレクションを filter
して map
して distinct
する、といったコードを綺麗に記述できます。
(->> (my-great-func-returns-coll) (filter #(not (nil? %))) ;nilをはぶく (map :name) ;:nameだけのリストにする (distinct)) ;重複をのぞいたリストを作る
->>マクロもスレッドマクロの基礎の一つで、このマクロがあるので、他のスレッドマクロの矢印の>が2つある場合は、フォームの最後に直前の式が挿入されるのだな、という暗黙の了解ができています。
as->マクロ
as->マクロはちょっと特殊で、->
マクロの不便なところを補う役割があります。
->
では、式が挿入される位置が「第1引数の位置」に固定されています。しかし、時には、式を別の位置に挿入したいこともあるはずです。そういう場合は、次のような、ちょっとアクロバティックなコードを書く必要がありました。
(-> {} (assoc :type ::book) (#(my-great-func arg1 % arg3)) (dissoc :result))
引数をひとつ受け取る匿名関数を生成して、それを呼び出すためにさらにカッコで囲うわけです。すると、その匿名関数の第1引数となる位置に、直前の式が挿入されるのです。
また、スレッドマクロの途中には関数しかおけないので、途中にログを出力する処理を挟みたいと思っても、ログを出力して引数をそのまま返すような関数を定義しなければいけません。簡単にかきたいからスレッドマクロを使っているわけで、ちょっと本末転倒なところがあります。
つまり、処理中の値(直前の式の結果値)を補足できればいいわけです。それを行うのが、as->
マクロの役割です。このような用途であるため、as->
マクロは、スレッドマクロの内側で使うことを前提にしています。もちろん、自分で第1引数に値を渡せばスレッドマクロ外でも動きますが、そうしても特に役立ちませんし。
さらに、as->
マクロは->
系統のスレッドマクロ (->, some->, cond->) でしか使えません(->>系統のマクロでは使えません。as->>というマクロはありません)。
(-> {:result "test"} (assoc :type ::book) (as-> x (my-great-func1 :a x :b) (my-great-func2 x)) (dissoc :result))
as-> x
によって、直前の結果値を x
に束縛できます。あとは、シンボル x
を使って、my-great-funcの好きな位置に、直前の結果値を挿入できます
。さらに、my-great-func1
の実行結果は再び x
に束縛されます。my-great-func2
に渡される x
は、(assoc :type ::book)
の結果ではなく、my-great-func1
の結果になります。
as->
を使うと、スレッディングの途中の値を、ログ出力することも出来ます。
(-> {} (assoc :test "1") (as-> x (do (println (str "x = " x)) x)) (assoc :test2 "2"))
この例では、(assoc :test "1")
の結果を x
に束縛し、ログ出力したあと、そのまま x
を返しています。(assoc :test "1")
の結果はそのまま (assoc :test2 "2")
に渡ります。スレッドマクロの途中に、手軽にログ出力を挿入したわけです。
as->
が導入されたおかげで、アクロバティックなコードを書く必要がなくなりました。
some->マクロ、some->>マクロ
some->
マクロは、->
マクロのシンプルな拡張です。同様に、some->>
マクロは ->>
マクロの拡張です。
スレッディング処理を実行中に、途中で結果が nil
になることがあります。nilになったらもう後続の処理は実行する意味がないとか(Clojureの多くの関数は nil
を渡すと単に nil
を返しますが、確実ではないし、無駄でもあります)、後続にJavaのメソッドをコールする部分があって、nil
を渡すと NullPointerException
になってしまう、ということはあり得ます。
some->
は、スレッディング途中に結果が nil
になったら、そこで処理を打ち切って nil
を返却します。
(defn die [v] (when (nil? v) (throw (NullPointerException.)))) (some-> [:a :b :c] next next next die)
ベクタ[:a :b :c]
に対して、「先頭要素を省いたシーケンスまたはnil
を返す」という動作をするnext
関数を3回使うと、結果はnil
になります。->
マクロであれば、最後にnil
がdie
関数に渡され、NullPointerException
がスローされます。
some->
であれば、3回目のnext
で結果がnil
になった段階でnil
を返すので、die
は実行されず、例外はスローされません。
実開発では、意外に使い勝手のいいスレッドマクロです。
cond->マクロ、cond->>マクロ
cond->
と cond->>
の違いも、some->, some->>
と同じく、引数の挿入位置だけです。それぞれ->
あるいは->>
と同じ場所に式を挿入します。
cond->
は、条件付きスレッドマクロです。次のように、条件式と、実行式のペアを記述していきます。
(let [v {:age 30, :place :japan}] (cond-> v (>= (:age v) 20) (assoc :adult true) (= :us (:place v)) (update-in [:recommended] conj "Bank of America") (= :japan (:place v)) (update-in [:recommended] conj "MUFG"))) ;;結果→ {:age 30, :place :japan, :adult true, :recommended ["MUFG"]}
cond->
は、初期値以降に、まず条件式を、次にその条件が真だった場合に実行する式を書きます。かならず2つをペアで書かなければなりません。
直前の式は、右側の式の第1引数の位置に挿入されます。左側(条件式)には挿入されないので注意が必要です。
cond
式と異なり、cond->
では、全ての条件式が評価され、真であれば、右の式を評価します。このとき、右の式に対してスレッディング処理が行われます(第1引数もしくは最終引数の位置に、直前の式が挿入される)。条件式が真でない場合は、対となる右側の式はスキップされ、次の条件式が評価されます。
前述の例なら、(= :us (:place v)
は真ではないので、(update-in [:recommended] conj "Bank of America")
は実行されず、最終結果の:recommended
にも"Bank of America"
は入っていません。
スレッドマクロを使いたいのだけど、スレッディング内の式の一部だけを条件付きで実行したいときに便利です。意外と、そういうことは多くあります。特にcond->>
マクロであれば、コレクション操作を条件付きで実行できるので便利です(特定のパラメータがオンのときだけ、最後にソートを実行する、など)。
まとめ
スレッドマクロは、関数スタイルで書くととても読みにくくなるコードを、劇的に読みやすくしてくれる便利なマクロで、人気も高いようです。標準のマクロ以外にも、オープンソースで公開されている独自マクロもあったりします。やはり「AしてBしてC」という処理は、同じ順序に書いたほうが分かりやすいですからね。中には、かなり実用的なものもあります。ここにある、Diamond Wandなんかはかなり使えます。
ただ、便利だからといってたくさん導入すると、コードが矢印だらけになって、読むのが大変になるかもしれません。Clojureに標準で存在するスレッドマクロを基本にして、ほかのものはチームと相談しつつ導入、というほうがよいかもしれないですね。
->
と->>
はClojureの初期から存在したので知っていても、cond->
やsome->, as->
は知らない、ということもあるかと思います。とても便利なので知っておいて損はないでしょう。
マクロを便利に使う
この記事はもともと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の指摘に感謝します