XMLを使うフレームワークの嫌なところ

この前、久しぶりにJavaのフレームワークを使う仕事をやりました。やはり、多くのビジネス・アプリケーション開発で採用されているJavaのフレームワークは素晴らし……くないぞぉ!なんだか使いづらいじゃないかぁ!

つらつらとその理由を考えてみたところ、「XMLが悪い」という結論になりました。今回は、その話を。

それにしても、Javaのフレームワークは使いづらいなんて書いちゃって大丈夫なのかなぁ、私ってば。……大丈夫、読んでいる人なんかほとんどいないから。

Apache Tilesを使って、ドハマリしました

今回作ったアプリケーションのページ構成は、以下のような感じでした。



ページ間で内容が同じヘッダー等の要素を複数回書くと保守性が下がってしまいますから、フレームワークの中にパーツをページに組み立てる何らかの仕組みがあるはず*1。今回フレームワークとして使用したのはSpringでしたので、Springのリファレンスを読んでみます。

はい、ありました。17.3 Tilesの記述によれば、Apache Tilesを使えばよいみたい。小さなプログラムを作って試してみます。以下のような感じ。

layout.xmlの抜粋

<definition name="/content" template="/WEB-INF/jsp/view.jsp">
  <put-attribute name="title"   value="コンテンツ" />
  <put-attribute name="content" value="/WEB-INF/jsp/content.jsp" />
</definition>
view.jsp

<%@ page contentType="text/html; charset=utf-8" %>
<%@ page pageEncoding="utf-8" %>

<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>

<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>Springを試してみました - <tiles:getAsString name="title" /></title>
    <link rel="stylesheet" href="/resources/css/spike-spring.css" />
  </head>
  <body>
    <div id="header" class="layout">
      <p>ここがヘッダー。</p>
    </div>
    <div id="content" class="layout">
      <tiles:insertAttribute name="content" />
    </div>
  </body>
</html>
content.jsp

<%@ page pageEncoding="utf-8" %>

<p>ここがコンテンツ。</p>

ふむふむ、XMLファイルでページの要素を定義して、その要素を実際に配置する部分はJSPでやるのね。配置の具体的なやり方は、Apache Tilesのタグ・ライブラリを使えばよい、と。うん、実に分かりやすい。

はい、動きました(ごめんなさい、テスト用なのでかなり画面がダサいです)。



プログラミングを進めましょう。今回作成したアプリケーションには、以下のようなcontentの部分がcontent-menuとcontent-bodyに分かれる画面もありました。



この要件に対応するには、2分割される場合用のdivided-content.jspを書いて、2分割する場合用のXMLを定義すればよいでしょう。以下のような感じです。

divided-content.jsp

<%@ page pageEncoding="utf-8" %>

<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>

<div id="content-menu" class="layout left-float">
  <tiles:insertAttribute name="content-menu" />
</div>

<div id="content-body" class="layout right-float">
  <tiles:insertAttribute name="content-body" />
</div>

<div class="reset-float">
</div>
layout.xmlの抜粋

<definition name="/divided-content" template="/WEB-INF/jsp/view.jsp">
  <put-attribute name="title" value="分割されたコンテンツ" />
  <put-attribute name="content" value="/WEB-INF/jsp/divided-content.jsp" /> 
  <put-attribute name="content-menu" value="/WEB-INF/jsp/content-menu.jsp" />
  <put-attribute name="content-body" value="/WEB-INF/jsp/content-body.jsp" />
</definition>
content-menu.jsp

<%@ page pageEncoding="utf-8" %>

<p>ここがコンテンツのメニュー。</p>
content-body.jsp

<%@ page pageEncoding="utf-8" %>

<p>ここがコンテンツの本体。</p>

はい、動かして……みたら、あれ?エラーになっちゃって動かない!



もー大慌てでApache Tilesの文書を読んで、締め切り間際にNesting Definitionsの記述に辿り着きました。

というわけで、XMLを以下のように修正します。

layout.xmlの抜粋

<definition name="/divided-content" template="/WEB-INF/jsp/view.jsp">
  <put-attribute name="title" value="分割されたコンテンツ" />
  <put-attribute name="content">
    <definition template="/WEB-INF/jsp/divided-content.jsp">
      <put-attribute name="content-menu" value="/WEB-INF/jsp/content-menu.jsp" />
      <put-attribute name="content-body" value="/WEB-INF/jsp/content-body.jsp" />
    </definition>
  </put-attribute>
</definition>

の下にを追加したわけですな。同じJSPを複数回使用する可能性もあるわけで、だから、この構造こそが正しい感じがします(Apache Tilesの文書を読むまではこの程度のことにすら気が付きませんでしたけど)。さっそく試してみましょう。



やったぁ、動きました。最終的に動いたからまぁよいんだけど、でも、Javaのフレームワークって、なんだか使うのが大変だなぁ……。

XMLは「外部」DSLです

え?お前に知識が無いのが悪いって?はい、おっしゃる通りです。すみませんした。え?お前はJavaやらないでClojureやっとけって?Clojureの仕事がねーんだよ!

さて、話は少し変わりますが、DSL(Domain Specific Language)という言葉があります。ある領域(Domain)に特化(Specific)した言語(Language)ですな。UNIX文化では様々なミニ言語が使われいて、これらがDSLの例になります(たとえば、正規表現とか)。あと、XMLを使用した設定ファイルも、DSLの1つ(設定という領域に特化した言語)なのだと考えます。

このDSLには、外部DSLと内部DSLがあります(あのマーチン・ファウラー先生による命名みたいです)。専用のコード・ジェネレーターとかインタープリターとかライブラリで解釈されるのが外部DSL、汎用言語の上に構築されたDSLが内部DSLになります。

で、XMLを使用した設定ファイルってのは、外部DSLなのだと思います。そして私は、外部DSLは使いづらいと考えているんです。

話を戻しましょう。Apache Tilesでは、<definition>は置き換えの指示、<put-attribute>は置き換えそのものという役割分担がされているのだと思います。<put-attribute>は置き換えそのものなので、JSPの中にさらに置き換えをする<tiles:insertAttribute>タグがあるとエラーになっちゃうのも、まぁ当然の話ですね。

……でも、あれ、ちょっと待ってください。<definition>と<put-attribute>の2つがあるわけですけど、本当に両方とも必要なのでしょうか?

Clojureでは「内部」DSLを使います

あーだーこーだ不満を言っているだけでは始まらないので、Clojureとcompojureとhiccupを使って、同じプログラムを書いてみます。以下がその結果(書いたコートのすべて)です。

(defproject spike-hiccup "1.0.0-SNAPSHOT"
  :description      "Spiking hiccup"
  :dependencies     [[org.clojure/clojure "1.4.0"]
                     [compojure           "1.1.1"]
                     [hiccup              "1.0.0"]]
  :dev-dependencies [[lein-ring           "0.7.1"]]
  :web-content      "public"
  :ring             {:handler spike-hiccup.core/app})
(ns spike-hiccup.core
  (:use [compojure core route handler])
  (:use [hiccup core page]))

(defn view
  [title content]
  (html5
   {:lang "ja"}
   [:head
    [:title (str "hiccupで試してみました - " (h title))]
    (include-css "/css/spike-hiccup.css")]
   [:body
    [:div.layout
     [:p "ここがヘッダー。"]]
    [:div.layout
     content]]))

(defn content
  []
  [:p "ここがコンテンツ。"])

(defn divided-content
  [content-menu content-body]
  (list
   [:div.layout.left-float
    content-menu]
   [:div.layout.left-float
    content-body]
   [:div.reset-float]))

(defn content-menu
  []
  [:p "ここがコンテンツのメニュー。"])

(defn content-body
  []
  [:p "ここがコンテンツの本体。"])

(defroutes main-routes
  (GET "/content"         [] (view "コンテンツ"
                                   (content)))
  (GET "/divided-content" [] (view "分割されたコンテンツ"
                                   (divided-content (content-menu)
                                                    (content-body))))
  (files "/"))

(def app
  (-> main-routes site))

コードの簡単な解説をさせてください。hiccupは、HTMLを生成するためのライブラリです。このhiccupを使うと、[:div ...]とかでHTMLを生成できます。上のコードのview関数がview.jspに相当するわけですな。で、defroutesマクロは、compojureが提供するHTTPリクエストと処理の対応付け機能です。"/content"というGETリクエストが来たら、(view ...)以下を評価した(「呼び出した」の意味です)結果を返す。このmain-routesが、layout.xml部分の役割も担っています。中を見ると、ほら、layout.xmlと似た内容が書かれているでしょ?

で、ここで注目していただきたいのですけど、Clojureのコードでは、「(レイアウト関数 レイアウト・パラメーター1 レイアウト・パラメーター2)」という表記で統一されています。レイアウト結果をパラメーターにしたい場合は、レイアウト・パラメーターを「(レイアウト関数 レイアウト・パラメーター1...)」に置き換えてるだけ。むちゃくちゃシンプルな構造になっています。

view等のHTML生成関数も見てください。<tiles:insertAttribute>に相当する部分も、ただ変数名を書いているだけです。変数名で表現できない場合(ロジックを介する必要がある場合)は、普通に関数を評価するコードを書くだけ。「[:title (str "hiccup..." (h title))]」とかね(strは文字列を生成する関数で、hはエスケープする関数)。やっぱり、めちゃくちゃシンプルです。

そもそもClojureでは、変数の値は「変数名」だけ、関数の評価をしたい場合は「(関数名 パラメーター ...)」と書くルールです。で、関数の評価をした結果をパラメーターにしたい場合は、パラメーターを「(関数名 パラメーター ...)」に置き換えるだけ。つまり、上で挙げたすべての話は、Clojureの文法を知っていれば調べるまでもない話なんですよ。Clojureも置き換えの指示も置き換えそのものも同じ文法でできちゃっているわけで、やたらめったらシンプルです。そう、前節の最後の疑問に対するClojureからの回答は、「<definition>と<put-attribute>の両方とも不要」なのです。

で、このhiccupは、内部DSLです。Clojureの中で実行できているわけですから「内部」。divided-content関数の中のdiv.layout.left-floatが<div class="layout left-float">に置き換えられているのはまさに文法の拡張で、HTML生成用の新しい言語なのだから「DSL」なのです。

私は、このようなシンプルで理解が簡単な構造を作れた理由は「hiccupが内部DSLだから」なのだと考えています。

私は、内部DSLが大好きです

内部DSLと外部DSLを比較してみましょう。

内部DSLの良い点として、拡張性が高いことが挙げられます。元の言語の機能を使い放題なわけですからね。対する外部DSLは、外部DSLを作成した時に考えた以上のことはできないという危険性があります。

反対に外部DSLの良い点としては、メインのコードから独立していることが挙げられるでしょう。XMLにJavaのコードは書けないわけですからね。対する内部DSLでは、HTMLを作っているんだかビジネス・ロジックを実行しているんだか分からないようなコードが書かれちゃう危険性があるわけです。

で、双方のメリットを勘案してどちらを取るのかと聞かれれば、私なら迷いなく内部DSLを選択します。複数の役割を持ったごちゃごちゃなコードを書いてしまうというのはDSLを使っていない場所でも発生しうる問題で、そのような品質が低いコードは問答無用で却下されるべきです。関数やメソッドは1つの機能のみを持つべきだというのは当たり前の話で、この程度のことに外部DSLを持ち出すまでもないと考えるためです。

いやいや、外部DSLのメリットはプログラムを書けない人でも保守が可能であることだ、という反論があるかもしれません。でも、layout.xmlの編集とmain-routesの編集なら、難易度は変わりませんよね?だから、どちらもプログラムを書けない人でも可能だと考えます。

コストの話をされる方がいるかもしれません。layout.xmlならビルドの手間が不要だからコストが小さいとの主張です。でも、Leiningenを使っているならばClojureのビルドはコマンド一発で完了するので、XMLにコスト面でのメリットはないと考えます。

テストのためにコードは入れ替えられないけれど外部DSLであるXMLならば入れ替えることができるとの主張も聞きます。でも、これはClojureでアスペクト指向の回に書いたようなテクニックを活用して動的にコードを置き換えれば、コードでも簡単にできる話です。なのでやっぱり、外部DSLを使う積極的な理由にはなりません。

結局のところ、外部DSLを使う本質的な理由は、「内部DSLを作れるほどに柔軟な言語を使っていない」なのだと考えます。これはあまりにも悲しい。ぜひ、Clojureで内部DSLを活用してみてください。Clojureはマイナーなので嫌だと言う場合は、メジャーな言語であるRubyを使用してみてください。Rubyでも内部DSLは活用されまくりですよ。

いやぁ、Clojure(やRuby)は素晴らしい!

*1:ごめんなさい。<jsp:include>の存在はすっかり忘れていました。