良いコ-ドへの道―普通のプログラマのためのステップアップガイド

第5回メタプログラミング―Excelを使ったDSLを作ろう―その4 Step3:リフレクションAPIで変換ルールを動的に適用する

Step3:リフレクションAPIで変換ルールを動的に適用する

Step3ではいよいよ読み取ったデータの変換処理を組み込みます。変換ルールはデータ項目に応じて異なるので、リフレクションAPIコラム参照を使用して動的に変換処理を適用する必要がでてきます。

下ごしらえとして、変換ルール用のConverterというインタフェースを用意し、変換ルールごとにConverterを実装するクラスを用意しますリスト3~6⁠。ここでは、IntegerConverter、DateConverter、TrimConverterの3つのクラスを作成しました。これらはStep1のstaticメソッドtoInteger、toDate、trimの処理をクラス化したものになります。

次に、Excelの設定ファイルに「変換ルール」列を追加します表3⁠。変換ルールには適用したい変換クラスのクラス名を指定します。

最後に、動的に変換オブジェクトを生成して実行するコードを追記しますリスト7⁠。

で設定ファイルの「変換ルール」列に記述されたクラス名を取得しています。

そして、が一番のポイントです。おーこわ注2。リフレクションAPIでクラス名の文字列から動的にClass型のオブジェクトを取得し、newInstanceメソッドでオブジェクトを生成しています。newInstanceメソッドは引数なしのコンストラクタを呼び出して、オブジェクトの生成を行います。つまり、のコードは次のコードと同じ意味になります。

Converter converter = new IntegerConverter();

リフレクションAPIを使用することで、コードに「具体的なクラス名やメソッド」を書くことなく、動的にプログラムを実行できるようになりました。今後、変換クラスの種類が増えていっても、Step3のコードは書き換える必要がありません。

リスト3 Step3:変換ルール用のインタフェース
public interface Converter {
    Object convert(Object value);
}
リスト4 Step3:変換ルール実装クラスIntegerConverter
public class IntegerConverter implements Converter {
    public Object convert(Object value) {
        return Integer.parseInt(value.toString());
    }
}
リスト5 Step3:変換ルール実装クラスDateConverter
public class DateConverter implements Converter {
    public Object convert(Object value) {
        try {
            return new SimpleDateFormat("yyyyMMdd")
                .parse(value.toString());
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }
}
リスト6 Step3:変換ルール実装クラスTrimConverter
public class TrimConverter implements Converter {
    public Object convert(Object value) {
        return StringUtils.trim(value.toString());
    }
}
リスト7 Step3:リフレクションAPIを使用したコード
private static class MessageParser {
...
    public void parse() throws Exception {
        while (index < bytes.getLength() - 1) {
            Map<String, Object> record =
                new HashMap<String, Object>();
            for (int i = 0; i < config.getRowSize(); i++) {
                DataRow row = config.getRow(i);
                String name =
                    (String) row.getValue("データ名称");
                int length =
                    ((BigDecimal) row.getValue("バイト数"))
                    .intValue();
                String ruleClassName =                  ┓
                    (String) row.getValue("変換ルール");  ┛①
                String value = getString(length);
                Class<?> clazz = Class.forName(ruleClassName);    ┓
                Converter converter =        &nbsp                    |
                    (Converter)clazz.newInstance();               ┛②
                Object newValue = converter.convert(value);     ―③
                record.put(name, newValue);
            }
            System.out.println(record);
        }
    }
...
}
表3 Excel設定ファイル(変換ルールを追加)
Noデータ名称長さ変換ルール
1送信日8gcw.gcw5.converter.DateConverter
2ユーザ名10gcw.gcw5.converter.TrimConverter
3メールアドレス20gcw.gcw5.converter.TrimConverter
4ポイント5gcw.gcw5.converter.IntegerConverter

考察1:ストラテジパターン

この例のように、処理のロジックを交換可能にして拡張性や保守性を高める手法のことを、デザインパターンでは「ストラテジ」注3と呼びます。ストラテジパターンではリフレクションを使って、動的にオブジェクトの生成を行うことが多いです。

考察2:複数の変換パターンを適用したい場合には?

Step3のコードには問題があります。

まず、適用できる変換ルールは1つのみという制限があります。ですので、⁠両端空白除去+Integer型への変換」といった複雑なことができません。解決策としては「複数の変換クラス名をカンマ区切りで列挙できるようにする」という方法が考えられます。コードのほうでは、クラス名をカンマで分割して、順番に変換処理を適用していきます。特に特殊な要件がなければ、これはシンプル解決策だと思います。

そのほかの問題として「変換ルールの生成時に引数を渡すことができない」という点があります。たとえば日付型へ変換する際に「yyyyMMdd」⁠yyyy-MM-dd」など任意の日付パターンを利用したいことがあります。現状ではパターンの指定がExcelからはできないので、パターンごとに変換クラスを作成する必要があります。これではとても面倒です。日付パターンを引数で受け取るコンストラクタを作成して、使用する日付パターンをExcelから指定できるようにすれば、変換クラスは1つで済みそうです。方法としては次の2つが考えられます。

方法1:DSLのルールで対応する

たとえば「[クラス名,引数1,引数2]」などのように、引数の記述ルールを決めて処理する方法です。簡単なものであればこれでもよいかもしれませんが、新しい構文が増えることに注意が必要です。また、定義の構文解析や引数の型変換などが必要になってきます。

 

方法2:式言語やスクリプト言語を利用する

OGNL(Object Graph Navigation Language)などの式言語や、JavaScript(Rhino)などスクリプト言語の式を書けるようにするという方法もあります。設定ファイルにJavaScriptの式を書けるようにすると表4のような記述が可能です。送信日の変換ルールとしてコンストラクタに引数を渡したり、ポイントの変換ルールでは配列を作成したりとやりたい放題です。動作するコードは本誌Webサイトの配布用サンプルに「Step4」として含んでいますので、参照してください。


どちらの方式であっても、⁠独自のDSL構文」が多くなり過ぎると利用者にとって理解しづらくなります。DSLの構文やルールは、熟考を重ねて利用者によってわかりやすい直感的なものを目指す必要があります。

表4 Excel設定ファイル(式を利用可能に)
Noデータ名称長さ変換ルール
1送信日8new DateConverter2("yyyyMMdd")
2ユーザ名10new TrimConverter()
3メールアドレス20new TrimConverter()
4ポイント5[new TrimConverter(),
new IntegerConverter()]

考察3:DSLの構文を改善するには?

改善1:変換ルールに短い別名を付ける

表3の「変換ルール」では、長い「フルパッケージ名付きのクラス名」を書く必要があります。これはちょっと面倒ですね。

表5のようにそれぞれのクラス名に「短い別名」を付けて、その別名で定義ができるようになると簡潔に書けそうです。これを実現するには、クラスと短い別名のマッピングを行う必要があります。マッピングは次のようなプロパティファイルで行うこととします。

date=gcw.gcw5.converter.DateConverter
trim=gcw.gcw5.converter.TrimConverter
integer=gcw.gcw5.converter.IntegerConverter

あとはフレームワーク側でマッピングのプロパティファイルを読み込んで、⁠短い別名⁠⁠→⁠フルパッケージ付きのクラス名」に変換する処理を追加すればOKです。ただし、別名を使える代償としてExcelにはクラス名ではない論理的な名前(たとえばdateやtrimなど)で指定することになります。論理的な名前と実際に処理を行うクラスとのマッピングについて知っておく必要があり、直接的な指定ではなくなるというデメリットもあります。そのほかにも、マッピングファイルを作成する手間が増えることも考慮する必要があるでしょう。

表5 Excel設定ファイル(短い別名に対応)
Noデータ名称長さ変換ルール
1送信日8date
2ユーザ名10trim
3メールアドレス20trim
4ポイント5integer

改善2:デフォルトで両端空白除去する

両端空白除去(trim)処理はほとんどすべてのデータ項目で必要です。ですので、デフォルトで両端空白除去することにして、両端空白除去をしない場合のみ「両端空白除去しない」という列に「○」を記述するのが良いでしょう。


DSLを取り入れた開発では、このように必要に応じて構文を拡張し、成長させるスタイルがお勧めです。

「短い別名」「両端空白除去しない」対応を行ったコードは配布用サンプルに「Step5」として含んでいますので、参照してください。

考察4:テストについて

このような「プログラムの外側」にプログラムの一部が移った場合、テストの範囲はどのようにすれば良いのでしょうか? 筆者は「プログラムの外側」も含めたユニットテストを書いてアプリケーションの動きをチェックすることが多いです。今回の例で言うとExcel形式の設定ファイル1つにつきMessageParser#parseメソッドに対するテストを書くのが良いでしょう。これは設定ファイルに対するテストを書くということです。

テストコードは配布用サンプルに含んでいます。テストコードは「gcw.gcw5.step3.SampleTest⁠⁠、Mainクラスをテストできるように修正したものは「gcw.gcw5.step3.Main2」です。

考察5:デバッグについて

DSLでは、エラーが発生した場合に原因がわかりにいという問題があります。発生場所や原因をきちんと通知するエラー処理をフレームワーク側で行うのがポイントです。たとえば、Excelの「長さ」列に値が入っていなかった場合、以下の詳細メッセージを持つ例外を投げるのが良いパターンです。

設定ファイル(config.xls)の文法が間違っています:列「長さ」 は必須です(4行目)

考察6:フレームワーク? それともDSL?

今回のサンプルはフレームワークでしょうか? これはフレームワークです。ではDSLでしょうか? これは意見が分かれるかもしれませんが、筆者はライトなDSLだと思っています。フレームワークとDSLとの境界線はあいまいです。⁠フレームワーク構築にDSLを用いると便利」という認識で問題ないでしょう。

今回のまとめ

今回はメタプログラミングの適用例としてExcelによるDSLを取り上げました。普段のプログラミングとは直接関係がなさそうな「メタプログラミング」ですが、意外と身近に存在していることがご理解いただければ幸いです。それでは次回をお楽しみに!#

お勧めサイト/書籍

Martin Fowler's Bliki「構文ノイズ」
DSLにおける構文上のノイズに関する考察が書かれています。RubyやJava、XMLなどでのDSLの構文例を挙げてその違いを比較しています。構文の違いで可読性や使い勝手がかなり異なることがよくわかります。
『増補改訂版 Java言語で学ぶデザインパターン入門』
結城 浩(著)、ソフトバンククリエイティブ、2004年
良い仕事をしたい普通のプログラマのためのデザインパターン入門本です。筆者はこの本のコードを2回写経して、さらに応用したアプリケーションをいくつか書いてみることで、抽象的に処理を扱う方法やフレームワークの作り方を学びました。たいへんわかりやすい本ですので、まだ手に取られていない人はぜひ読んで、ぜひ書いてみてください

おすすめ記事

記事・ニュース一覧