倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

FactoryBuilderSupport よ、我に力を! (9) : 自作ビルダーのための GroovyDSL

これは FactoryBuilderSupport を使うかどうかに関係ありませんが、独自のビルダーを作っていると自分でも「どんなノード作ったっけ?」みたいなときがよくあります(よネ?) JavaDoc をきちんと書くとか、コーディングする前にノードをすべて洗い出しておくとかしてればいいんでしょうけど、現実には意思とは弱いもの。 そこで、環境が限定されるけどちょっとはマシな解決方法を。

で、その方法というのは、以前に記事に書いた「GroovyDSL」を使って、コンテンツ・アシストで定義されているノードを補完できるようにしてやろう!ってものです。 GroovyDSL ファイルを書かないといけないので「JavaDoc をきちんと書く」ってのとあまり変わらない気もしますが、ビルダーを使う際にはかなり有難味があると思います。 ちなみこの方法は、開発環境として IntelliJ IDEA を使ってる場合にしか使えませんのでご了承を。

コンテキスト


GroovyDSL は何らかの要素(クラスなど)に対してプロパティやメソッドを付加しているように見せます。 メタクラスによるプログラミングのように実際にそれらを付加しているわけではなく、あくまで IDE (IntelliJ IDEA) にそう認識させているだけです。 で、GroovyDSL ファイル(拡張子 .gdsl)に書く必要があるのは

  • プロパティやメソッドを付加する対象であるコンテキスト
  • コンテキストに付加するプロパティやメソッドの候補

です。 それぞれを見ていきましょう。

コンテキスト

まずは付加対象のコンテキスト。 今の場合、これはビルダークラスです。 前回までに作っていたサンプルでは PoiBuilder クラスに当たります。 それに加えて、スクリプトにノードだけを

workbook{
    row(0){
        cell(0, cellType:CELL_TYPE_STRING)
        cell(1, cellType:CELL_TYPE_BOOLEAN)
    }
}

のように書いて使いたい場合もあるかもしれないので*1、スクリプトに対しても同様のプロパティやメソッドを付加することにしましょう。 したがって、GroovyDSL ファイルは

contributor(ctype:['org.waman.tools.poi.PoiBuilder', context(scope: scriptScope())]){
    ...
}

のようにして、「...」の部分にプロパティやメソッドの候補を書いていきます。

ノードの候補

ビルダーのノードはクロージャ型のプロパティとして宣言するとよいようです*2 *3

contributor(ctype:['org.waman.tools.poi.PoiBuilder', context(scope: scriptScope())]){

    property name:'workbook', type:'groovy.lang.Closure<org.apache.poi.ss.usermodel.Workbook>'
    ...
}

クロージャは Java クラスとして見ると返り値の型に対応する型パラメータをとるので、そこにノードの返り値の型を指定します。 ノードの属性に指定できるプロパティ名も候補として定義してコンテンツ・アシストできればいいんですが、この方法では無理そうです。

定数に対応するプロパティの候補

ついでに、前回の記事に書いた Cell インターフェースに定義されているプロパティも、PoiBuilder のプロパティとして参照できるようにしておきましょう:

contributor(ctype:['org.waman.tools.poi.PoiBuilder', context(scope: scriptScope())]){
    ...

    ['CELL_TYPE_NUMERIC',
     'CELL_TYPE_STRING',
     'CELL_TYPE_FORMULA',
     'CELL_TYPE_BLANK',
     'CELL_TYPE_BOOLEAN',
     'CELL_TYPE_ERROR'].each{ property name:it, type:'int' }
}

GroovyDSL ファイル内ではライブラリのクラスを参照できないようなので、結構、力業で定義しています。 可能ならビルドプロセスの途中で自動生成した方がいいかも。

完成版


完成版の GroovyDSL ファイルはこんな感じになります:

contributor(ctype:['org.waman.tools.poi.PoiBuilder', context(scope: scriptScope())]){

    property name:'workbook', type:closure(poi('Workbook'))
    property name:'sheet', type:closure(poi('Sheet'))
    property name:'row', type:closure(poi('Row'))
    property name:'cell', type:closure(poi('Cell'))
    property name:'cellValue', type:closure('java.lang.Object')

    // Constants of Cell
    ['CELL_TYPE_NUMERIC',
     'CELL_TYPE_STRING',
     'CELL_TYPE_FORMULA',
     'CELL_TYPE_BLANK',
     'CELL_TYPE_BOOLEAN',
     'CELL_TYPE_ERROR'].each{ property name:it, type:'int' }
}

def poi(String s){ 'org.apache.poi.ss.usermodel.'+s }
def closure(String s){ "groovy.lang.Closure<$s>" }

GroovyDSL ファイル内にもメソッドを定義できるので、Groovy のクロージャの完全修飾名ややパッケージ名を書くのに楽してます。

ドキュメントを書くのと同様、作成したノードが増えると GroovyDSL ファイルを管理するのが結構面倒になりますが、開発に際してサンプルコード(テストコード)を書いておけば、IntelliJ IDEA でそれらのファイルを開くだけで、(候補として定義されているノードやプロパティをカラーリングしてくれるので)ノードの候補がきちんと書かれているか確認できます。 通常のドキュメントと違って、きちんと書いていればそれなりのリアクション(?)があるので、まだモチベーションを持って管理できるのではないでしょうか。

プログラミングGROOVY

プログラミングGROOVY


Groovy for Domain-specific Languages Intellij Idea In Action

*1:このように書かれたスクリプトは、FactoryBuilderSupport クラスの build() メソッドによって構築を実行することができます。

*2:よく考えると謎ですけど・・・ 何か Groovy の略記法を考慮すればうまくいくのかな?

*3:この宣言の方法は、IntelliJ IDEA の Groovy プラグインで採用されている方法で、SwingBuilder に対する GroovyDSL ファイルもこの方法で書かれています。 「《IntelliJ IDEA のインストール・フォルダ》/plugins/Groovy/lib/standardDsls」フォルダにこのファイルがあります。 ちなみに、ビルダーのノードに対する GroovyDSL の他にもカテゴリ・クラスに対するものなどもあります。 GroovyDSL ファイルを書こうとしてる人は必読。