倭マン's BLOG

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

FactoryBuilderSupport よ、我に力を! (8) : ビルダーをちょっと使いやすくするために

前回Apache POI (のスプレッドシート)を構築するビルダーを構築しました。 そこではノードを構築するビルダーはそれなりのコーディングをしましたが、(JavaBean としての)プロパティは属性として指定すると自動的に注入されるようにしました。

で、プロパティの値として、数値や文字列を指定する場合は特に問題はないのですが、そうではなくて値として列挙型や定数*1などを指定する場合は、それらが定義されているクラスやインターフェースを import する必要があります。 IDE を使って開発する場合は大した手間でもありませんが、そうでない場合はクラスなどの完全修飾名なんて覚えてないのが普通なので*2、こういった import 文を書かなくてもいいようにしましょう。

では具体的に・・・


もうすこし具体的に見てみましょう。 前回作成したビルダーではセルのタイプを指定するには以下のようにする必要がありました:

import org.waman.tools.poi.PoiBuilder
import org.apache.poi.ss.usermodel.Cell

def workbook = new PoiBuilder().workbook{
    row(0){
        cell(0, cellType:Cell.CELL_TYPE_STRING)
        cell(1, cellType:Cell.CELL_TYPE_NUMERIC)
        cell(2, cellType:Cell.CELL_TYPE_BLANK)
        cell(3, cellType:Cell.CELL_TYPE_ERROR)
    }
}

new File('sample.xls').withOutputStream { workbook.write(it) }

セルのタイプが何かってのはあまり関係なくて、属性の値に指定するときに Cell インターフェースを参照する必要があるのがここでの問題です。 まぁ言ってみれば、Cell インターフェースの import がちょっと面倒だね!ってことです。 ついでに、Cell インターフェースに定義されている定数を使用する際の、'Cell.' ってのも書かなくていいようにします。 つまり、ビルド・スクリプトをこんな感じに書けるようにします:

import org.waman.tools.poi.PoiBuilder
// Cell を import する必要なし

def workbook = new PoiBuilder().workbook{
    row(0){
        // CELL_TYPE_XXXX を直接書ける
        cell(0, cellType:CELL_TYPE_STRING)
        cell(1, cellType:CELL_TYPE_NUMERIC)
        cell(2, cellType:CELL_TYPE_BLANK)
        cell(3, cellType:CELL_TYPE_ERROR)
    }
}

new File('sample.xls').withOutputStream { workbook.write(it) }

PoiBuilder の import は仕方ないですね*3。 また、CELL_TYPE_XXXX にどんなものがあるかは、結局 API ドキュメントを見るなり IDE の機能を使うなりしないといけません。

解決策は・・・


どのようにこれを解決するかというと、ビルダー(PoiBuilder) に対して 'CELL_TYPE_XXXX' というプロパティにアクセスすると、対応する Cell のフィールド Cell.CELL_TYPE_XXXX が返されるようにします。 実装の詳細はどうでもいいですが、これはビルダーの getProperties() メソッドをオーバーライドすることで実現できます:

import java.lang.reflect.Field
import java.lang.reflect.Modifier
import org.apache.poi.ss.usermodel.Cell

class PoiBuilder extends FactoryBuilderSupport{

    private static final Map<String, Object> CONSTANTS

    static{
        def map = [:]
        Cell.declaredFields.each{ Field f ->
            if(isPublicStaticFinal(f.modifiers)){
                map.put(f.name, f.get(null))
            }
        }
        CONSTANTS = map.asImmutable()
    }
    
    /** public static final なフィールドを定数と見なしています。 */
    private static boolean isPublicStaticFinal(int mod){
        return Modifier.isPublic(mod) && Modifier.isStatic(mod) && Modifier.isFinal(mod)
    }

    @Override
    Object getProperty(String name){
        if(CONSTANTS.containsKey(name)){
            // 定数が参照されれば対応するフィールドを返します。
            return CONSTANTS.get(name)
        }else{
            return super.getProperty(name)
        }
    }

    ...
}

Cell 以外の型に定義されている定数も、よく使いそうなものは同じように登録しておくといいでしょう。 あまりあれこれ登録すると、クラスの初期化にコストがかかりますが。

プログラミングGROOVY

プログラミングGROOVY


Groovy for Domain-specific Languages

Groovy for Domain-specific Languages

*1:ここでは public static final なフィールドを意図しています。

*2:列挙型や定数の名前も覚えてないことの方が多いかも知れませんが(笑)

*3:FactoryBuilderSupport の build() メソッドを使えば何とかできますけどネ。