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

第5回メタプログラミング―Excelを使ったDSLを作ろう―その2 Step1:ベタなコードで書いてみる

ExcelでDSLしてみよう

それでは実際にExcelを使った外部DSLっぽいミニフレームワークを作成してみましょう。通常のプログラムやXMLと比較して、DSLの表現方法としてのExcelには次のメリットとデメリットがあります。

メリット

Excelは表ですので、表形式のデータを見たまま表現できるのが一番のメリットです。また、セルに色をつけたりコメントも自由自在に書けるなど視覚的な表現力も高いといえます。もとになる仕様書などがそもそもExcelだったりしますので、加工したりコピー&ペーストして利用することも簡単です。

デメリット

デメリットとしては、変更履歴の差分(diff)が取りにくい点が挙げられます。また、テキストエディタで編集できない、セルに構文を含んだ長い文字を書くのは辛いなどの問題点もあります。

今回のお題

今回のお題は「固定長電文解析処理」です。メインフレームの時代より「固定長フォーマット」の電文が利用され続けてきました。現在でも「固定長電文」はデータ交換のメジャーなフォーマットです。Javaには固定長電文を解析する標準APIはありませんので、自作する必要があります。そこで今回は、固定長電文の解析にExcelによるDSLを適用します。

例で使用する「固定長電文」図1のようなものです。読みやすいように改行を入れていますが、実際はすべてつながって1行になります。

表1が電文のフォーマットやルールを記述した「電文仕様書」です。電文仕様書には、先頭から8バイトが送信日、9バイト目からの10バイトがユーザ名といった情報や、空白除去、データの型といった変換ルールの仕様などが記載されています。

この仕様書をもとに固定長電文を解析してから、目的の処理(たとえば「DBにデータ追加する」など)を行います。

図1 Step1のクラス図
1234567890123456789012345678901234567890123
20081101user1     [email protected]     10
20081101user2     [email protected]      0
20081101user3     [email protected]    100
20081102user4     [email protected]     80
20081102user5     [email protected]     55
表1 電文仕様書
Noデータ名称開始長さ変換ルール
1送信日18Date型に変換
(yyyyMMdd)
2ユーザ名910両端空白除去(trim)
3メールアドレス1920両端空白除去(trim)
4ポイント395両端空白除去(trim⁠⁠、
Integer型に変換

Step1:ベタなコードで書いてみる

まずはDSLなどを使わずにべたにコードを書くとリスト1のようになります。

まずで固定長電文のファイルを読み込み、バイト配列に変換しています。バイト配列はそのままでは「配列の一部を取り出す操作」などが面倒です。ですので、「ByteArray」という自作ユーティリティクラスを使用して、バイト配列を扱いやすくラップしています。

parseメソッドはデータの読み込み処理です。データがなくなるまで読み込み処理を繰り返します。でそれぞれのデータ項目のサイズだけ順番にデータを読み込んでいます。同時にDate型への変換(toDate)や両端文字列の除去(trim⁠⁠、Integer型への変換(toInteger)を行っています。で1件分のデータを標準出力へ出力しています。実際に実行してみると図2の出力が得られます。

リスト1 Step1:ベタなコード
public static void main(String[] args) throws Exception {
    byte[] messages = FileUtils.readFileToByteArray(  ┓
        new File("data.txt"));                        ┛①
    MessageParser parser = new MessageParser(messages);  ―②
    parser.parse();
}

private static class MessageParser {
    private int index = 0;
    private final ByteArray bytes;

    public MessageParser(byte[] bytes) {    ┓
        this.bytes = new ByteArray(bytes);  |
    }                                       ┛③

    public void parse() {
        while (index < bytes.getLength() - 1) {
            Map<String, Object> record =
                new HashMap<String, Object>();
            record.put("送信日", toDate(getString(8)));           ┓
            record.put("ユーザ名", trim(getString(10)));           |
            record.put("メールアドレス", trim(getString(20)));       |
            record.put("ポイント", toInteger(trim(getString(5))));  ┛④

            System.out.println(record);     ―⑤

        }
    }

    private String getString(int length) {
        String value = bytes.getString(index, length);
        index += length;
        return value;
    }
    private static Integer toInteger(String value) { ... }
    private static String trim(String value) { ... }
    private static Date toDate(String value) { ... }
}
図2 実行結果
{送信日=Sat Nov 01 00:00:00 JST 2008, ポイント=10, ユーザ名=user1, メールアドレス[email protected]}
{送信日=Sat Nov 01 00:00:00 JST 2008, ポイント=0, ユーザ名=user2, メールアドレス[email protected]}
{送信日=Sat Nov 01 00:00:00 JST 2008, ポイント=100, ユーザ名=user3, メールアドレス[email protected]}
{送信日=Sun Nov 02 00:00:00 JST 2008, ポイント=80, ユーザ名=user4, メールアドレス[email protected]}
{送信日=Sun Nov 02 00:00:00 JST 2008, ポイント=55, ユーザ名=user5, メールアドレス[email protected]}

考察1:MessageParserがクラスになっているのはなぜ?

MessageParserクラスは連載の前回でもお勧めしたインナークラスとして実装されています。これはバイト配列の現在の読み込み位置である変数indexをフィールドとして保持したいからです。

もしクラス化せずにローカル変数で保持しようとすると、以下のようなコードになるはずです。

Map<String, Object> record = new HashMap<String, Object>();
record.put("送信日", toDate(bytes.getString(index, 8)));
index += 8;
record.put("ユーザ名", trim(bytes.getString(index, 10)));
index += 10;

また、別の方法としては自作のByteArrayクラス自体に「現在の読み取り位置」を保持する機構を持たせるという手もあります。その場合、呼び出し元は「何バイト目まで読み込んだのか?」というのは知る必要がなく、⁠次の8バイトを読み込む」⁠まだ読み込めるか?」などを問い合わせするだけでよくなり、よりカプセル化が進んだ状態となります。その際クラス名は「ByteArray」ではなく「ByteReader」に、メソッド名「getString」「readAsString」にするなど、より適切な名前にリファクタリングしたほうがよいでしょう(名前付けに関しては本誌Vol.45の本連載第2回を参照してください⁠⁠。コードは次のようになるでしょう。

ByteReader reader = new ByteReader(bytes);
String sendedDate = reader.readAsString(8);
String userName = reader.readAsString(10);

考察2:これからどうする?

リスト1はベタに書かれていて、シンプルでわかりやすいコードになっていますね。今回指定された仕様ぐらいであれば、このコードは十分に「良いコード」です。めでたしめでたし……だと話が終わってしまうので、もう少しいろいろな状況を考えてみましょう。

今回は電文内のデータ項目は4種類(送信日、ユーザ名、メールアドレス、ポイント)ですが、データ項目が100を超えるような電文はざらにあります。そのような場合、のデータを読み取る処理が100行以上続くことになり、可読性がかなり悪くなりそうです。仕様書を見ながらプログラムへ反映するのも大変でしょう。ですのでStep2では、⁠Excelの仕様書」を読み込んで、その仕様書に定義されたとおりに処理が実行されるように書き換えてみましょう。

おすすめ記事

記事・ニュース一覧