Java プログラミングのダイナミックス: 第 3 回 実用的なリフレクション

コマンド・ライン引数のフレームワークを構築する

コマンド・ライン引数処理は不快な作業のうちの 1つであり、過去に何度それを行ってきたとしても巡り続けるように思われます。何度も何度も同じコードのバリエーションを書くのではなく、引数処理のジョブを単純化するためにリフレクションを使用してはどうでしょう?Java コンサルタントの Dennis Sosnoski 氏はその方法を解説しています。この記事では、Dennis 氏はコマンド・ライン引数でそれ自身を実際に処理するオープン・ソース・ライブラリーについて概説いたします。

Dennis Sosnoski, President, Sosnoski Software Solutions, Inc.

Photo of Dennis SosnoskiDennis Sosnoski はシアトル地域にある Java 技術のコンサルティング会社、Sosnoski Software Solutions, Inc. の創立者で、主席コンサルタントでもあり、また XML や Web サービスに関するトレーニングやコンサルティングの専門家でもあります。彼のプロとしてのソフトウェア開発経験は 30年以上に渡り、ここ数年はサーバー側の XML 技術や Java 技術に注力しています。Dennis は、全米各地で行われる会議で頻繁に講演を行っています。また、Java クラスワーキング技術を基に構築された、オープンソースの JiBX XML Data Binding フレームワークの中心開発者でもあります。



2003年 7月 15日

前回の記事では、Java リフレクション API を紹介し、そのいくつかの基本的な機能についてざっと説明をしました。またリフレクションの性能についても調査し、リフレクションがアプリケーションにおいて使用されるべき/使用されないべきのガイドラインを示したところで前回は終了しました。今回は、リフレクションの強み・弱みにぴったりと合致するようなアプリケーションを見ていくことで、コマンド・ライン引数処理用のライブラリーについての話を進めていくことにします。

まずは解決すべき問題を定義し、実装コードへと話しが進む前にライブラリーのインタフェースを設計することにします。私が実際にしてきたライブラリー開発は、それほど構造化されていませんでした。--共通のコード・ベースを使用する一群のアプリケーションの既存コードを単純化することから取り掛かり、そこから一般化をしました。この記事での "定義-設計-構築" という順序は、開発プロセスの詳細な説明よりはるかに簡潔です。しかし、ライブラリーを作成する過程において、私は最初の仮定のうちのいくつかを改訂して、ライブラリー・コードを仕上げました。おそらく、これがみなさんのリフレクションベースのアプリケーション開発のモデルとして役に立つことがお分かりになることでしょう。

問題の定義

私はコマンド・ラインからの引数を使って、多くの Java アプリケーションを作成してきました。たいていのアプリケーションは非常に小さなものから着手したものの、いくつかは当初のプランをはるかに越えて大きくなってしまいました。このプロセスにより私が気づいた点から、標準的なパターンができあがりました。

  1. 1つまたは 2つの必須引数を特定の順番で指定することから始めます。
  2. アプリケーションで行われるより多くのことを考え、引数を追加していきます。
  3. 毎回すべての引数を入力するのはうんざりするので、デフォルト値を使うことで、いくつかの引数は省略可能にします。
  4. 引数の順番は忘れてしまうので、引数指定は任意の順でできるようにコードを変更します。
  5. 興味を持っている人々にアプリケーションを使ってもらいます。彼らは、どんな引数が必要とされているかを知りません。ですから、引数のエラー・チェックや「ヘルプ」の説明を追加します。

ステップ 5 に到達するまでに、私はいつも始めの段階で全プロセスを開始してしまったことを後悔します。しかし幸運にも、私は後の段階をきれいに忘れる傾向があるので、1、2週間の内になお一層シンプルな別の小さなコマンドライン・アプリケーションを考えることでしょう。その後、この醜いサイクルが繰り返されるのは時間の問題ですけれど。

コマンド・ライン引数処理を備えた役に立つライブラリーはいくつか存在しています。しかし、私はそれらを無視し、この記事では私の方法で進めていきたいと思います。これは「ここで作られなかったから」ということではなく、むしろ例として引数処理を使用するためです。偶然にも、リフレクションの強み・弱みは引数処理ライブラリーの要件にぴったりと合っています。特に、引数処理ライブラリーには、以下のような要件があります。

  • 様々なアプリケーションをサポートする柔軟なインタフェースが必要です。
  • 各アプリケーションで簡単に設定ができなければなりません。
  • 引数は一度処理されるだけなので、トップ・パフォーマンスは必要ではありません。
  • コマンド・ライン・アプリケーションは通常セキュリティ・マネージャーなしで動いているので、アクセス・セキュリティー問題は生じません。

このライブラリー内での実際のリフレクション・コードは、全体の実装のごく一部にしか相当しません。したがって、ここではリフレクションに最も関係する側面について注目することにします。ライブラリー (また、簡単なコマンドライン・アプリケーション用にライブラリーを使用する方法) の詳細については、参考文献セクションの Web サイトへのリンクから参照してください。

設計について

アプリケーションが引数データにアクセスする最も便利な方法は、おそらくアプリケーションのメイン・オブジェクトのフィールドを使うことです。例えば、事業/経営計画を生成するアプリケーションを作成するとします。事業計画が簡潔かそうでないかを制御する boolean 型フラグ、初年度の収益用に int、予定された合成収入成長率用の float、および製品説明に String を使用することができます。アプリケーションのパラメーター 操作に影響を与えるこれらの変数を呼び出すことで、実際の引数 (パラメーター変数の値) と区別することができます。また、これらのパラメーターのフィールドを使用すれば、アプリケーション・コード内のどこででもこれらを簡単に利用できるようになります。リスト 1 で示されるように、フィールドを使用すると定義で簡単にパラメーターにデフォルトをセットすることもできます。

リスト 1. 事業計画の生成 (一部)
public class PlanGen {
    private boolean m_isConcise;          // rarely used, default false
    private int m_initialRevenue = 1000;  // thousands, default is 1M
    private float m_growthRate = 1.5;     // default is 50% growth rate
    private String m_productDescription = // McD look out, here I come
        "eFood - (Really) Fast Food Online";
    ...
    private int revenueForYear(int year) {
        return (int)(m_initialRevenue * Math.pow(m_growthRate, year-1));
    }
    ...

リフレクションにより引数処理ライブラリーがアプリケーション・コードに特別なフックなしで値を設定できるようになり、プライベート・フィールドへ直接アクセスが可能となります。しかし、ライブラリーがこれらのフィールドを特定のコマンドライン引数に関連づける方法が必要となります。引数とフィールド間の関連をライブラリーへ伝える方法を定義する前に、コマンドライン引数のフォーマット形式を決めておく必要があります。

この記事では、UNIX 規約の単純化されたバージョンでコマンドライン・フォーマットを定義します。パラメーター用の引数値は、任意の順で、1文字以上のパラメーター・フラグ (実際のパラメーター値ではなく) が与えられることを示唆するハイフンを伴って指定することができます。事業計画の生成については、これらのパラメーター・フラグ文字を選定しています。

  • c -- 簡潔なプラン
  • f -- 初年度の収入 (数千ドル)
  • g -- 成長率 (年1回の乗数)
  • n -- 製品名

boolean 型パラメーターは、値をセットするためのフラグ文字だけが必要となりますが、他のパラメーターにはある種の付加的な引数情報が必要です。String 値のパラメーターは、コマンドラインのフラグ文字の後に続きますが、数値の引数値はパラメーター・フラグ文字の直後にセットするようにします (つまり、フラグ文字としては数字を使用できません)。最後に、必須パラメーター (事業計画生成の出力ファイル名のような) がある場合は、コマンドラインの省略可能なパラメータ値の後に続けます。これらの規約からすると、事業計画生成のためのコマンドラインは以下のようになります。

java PlanGen -c -f2500 -g2.5 -n "iSue4U - Litigation at Internet Speed" plan.txt

各引数の意味は次のとおりです。

  • -c -- 簡潔なプランの生成
  • -f2500 -- 2,500,000ドルの初年度の収入
  • -g2.5 -- 1年当たり 250%の成長率
  • -n "iSue4U . . ." -- 商品名 "iSue4U . . ."
  • plan.txt -- 必須パラメーターの出力ファイル名

この時点で、引数処理ライブラリーの基本的な機能仕様ができました。次のステップは、ライブラリーを使用するアプリケーション・コード用に特定のインタフェースを定義することです。


インタフェースの選択

1回の呼出しで、実際にコマンドライン引数処理を扱うことができますが、アプリケーションにはまずライブラリーに特定のパラメーターを定義する手段が必要です。これらのパラメーターには、いくつかの異なるタイプがあります (事業計画生成の例の場合は、booleanintfloatjava.lang.String です)。さらにそれぞれのタイプの中には特有な要件をもつものがあるかもしれません。例えば、フラグ文字が存在する時は常に boolean パラメーターを true に定義するのではなく false として定義してもよいですし、int 値の有効な範囲を定義することも実用的です。

すべてのパラメーター定義には基本クラスを使用し、個々のタイプのパラメーター用には基本クラスをサブクラス化して、異なった要件に対処することにします。このアプローチでは、基本パラメーター定義クラスの配列の例として、アプリケーションにライブラリーへのパラメーター定義をさせていますが、実際の定義ではそれぞれのパラメーター・タイプに一致する特定のサブクラスを使用することができます。事業計画生成の例については、リスト 2 のような形式をとることになります。

リスト 2. 事業計画の生成用のパラメーター定義
private static final ParameterDef[] PARM_DEFS = {
    new BoolDef('c', "m_isConcise"),
    new IntDef('f', "m_initialRevenue", 10, 10000),
    new FloatDef('g', "m_growthRate", 1.0, 100.0),
    new StringDef('n', "m_productDescription")
}

配列に定義されたパラメーターを使えば、アプリケーション・プログラムから引数処理コードへの呼出しを、スタティック・メソッドの呼出しと同じくらい単純にさせることができます。パラメーター配列に定義された数以上に引数を追加するには (要求値または可変長の値のいずれか)、引数処理コードへの呼出しに実際に処理される引数の数をリターンさせます。これにより、アプリケーションに追加引数をチェックさせ、それらを適切に使用させることができます。最終的にはリスト 3 のようになります。

リスト 3. ライブラリーの使用
public class PlanGen
{
    private static final ParameterDef[] PARM_DEFS = {
        ...
    };
    public static void main(String[] args) {
    // if no arguments are supplied, assume help is needed
        if (args.length > 0) {
        // process arguments directly to instance
            PlanGen inst = new PlanGen();
            int next = ArgumentProcessor.processArgs
                (args, PARM_DEFS, inst);
            // next unused argument is output file name
            if (next >= args.length) {
                System.err.println("Missing required output file name");
                System.exit(1);
            }
            File outf = new File(args[next++]);
            ...
        } else {
            System.out.println("\nUsage: java PlanGen " +
            "[-options] file\nOptions are:\n  c  concise plan\n" +
            "f  first year revenue (K$)\n  g  growth rate\n" +
            "n  product description");
        }
    }
}

ここでただ一つ残っているのは、エラー報告処理に関してです (未知のパラメーター・フラグ文字、あるいは範囲外の数値など)。この目的のために、何らかのエラーが生じたらスローされる unchecked exception (未検査例外) として ArgumentErrorException を定義しています。この exception にひっかからなければ、エラーメッセージやダンプされているスタック・トレースをコンソールに出力させて、直ちにアプリケーションは強制終了します。別の方法としては、コード内でこの exception を直接キャッチして対処することもできます (例えば使用法と一緒にエラーメッセージを出力するなど)。


ライブラリーの実装

ライブラリーが筋書きどおりにリフレクションを使用するには、パラメーター定義の配列で指定されたフィールドを調べて、対応するコマンドライン引数からこれらのフィールドへ適切な値を格納する必要があります。実際のコマンドライン引数に必要なフィールド情報を調べることで、このタスクを処理することはできるかもしれませんが、私は処理とフィールド情報の検索を分けることにしました。前もってすべてのフィールドを調べ、それから引数の処理中に調べておいた情報を使用します。

前もってすべてのフィールドを調べておくことは、リフレクションの使用に関する潜在的な問題のうちの 1つを除去する防衛のプログラミング・ステップとなります。もし必要なフィールドを調べるだけならば、何かが間違っていることに気付かずに、(対応しているフィールド名のタイプミスなどで) パラメーター定義を簡単に壊してしまうことでしょう。フィールド名が String で指定されていれば、コンパイル・エラーは起きません。また、壊れたパラメーター定義に一致する引数がコマンドラインで指定されない限りは、プログラムはきちんと実行されるでしょう。この種の隠れたエラーにより壊れたコードが発売されやすくなってしまいます。

実際に引数処理を行う前にフィールド情報を調べる例として、フィールド検索を処理する bindToClass() メソッドを備えたパラメーター定義のための基本クラスの実装をリスト 4 に示しておきます。

リスト 4. パラメーター定義の基本クラス
public abstract class ParameterDef
{
    protected char m_char;          // argument flag character
    protected String m_name;        // parameter field name
    protected Field m_field;        // actual parameter field
    protected ParameterDef(char chr, String name) {
        m_char = chr;
        m_name = name;
    }
    public char getFlag() {
        return m_char;
    }
    protected void bindToClass(Class clas) {
        try {
        // handle the field look up and accessibility
            m_field = clas.getDeclaredField(m_name);
            m_field.setAccessible(true);
            } catch (NoSuchFieldException ex) {
            throw new IllegalArgumentException("Field '" +
                m_name + "' not found in " + clas.getName());
        }
    }
    public abstract void handle(ArgumentProcessor proc);
}

実際のライブラリー実装には、私がこの記事で言及してきた以上のクラスがいくつか含まれています。大部分はライブラリーのリフレクションとは無関係であるので、ここでその全てを検討するつもりはありません。言っておきたいことは、ArgumentProcessor クラスのフィールドとしてターゲット・オブジェクトを格納し、このクラス内でパラメーター・フィールドの実際の設定を実装することに決めたということです。このアプローチは、引数処理にシンプルなパターンを提供しています。ArgumentProcessor クラスは、パラメーター・フラグを調べるために引数をスキャンし、(常に ParameterDef のサブクラスになる) 各フラグと対応するパラメーター定義を調べて、handle() メソッドを呼び出します。handle() メソッドは、引数値を解釈した後 ArgumentProcessorsetValue() メソッドを呼び出します。リスト 5 は、コンストラクターおよび, setValue() メソッドで呼出しをバインドしているパラメーターを含む ArgumentProcessor クラスの一部です。

リスト 5. メインのライブラリー・クラスの一部

リスティングを見るにはここをクリック

リスト 5. メインのライブラリー・クラスの一部

public class ArgumentProcessor
{
    private Object m_targetObject;  // parameter value object
    private int m_currentIndex;     // current argument position
    ...
    public ArgumentProcessor(ParameterDef[] parms, Object target) {
        // bind all parameters to target class
        for (int i = 0; i < parms.length; i++) {
            parms[i].bindToClass(target.getClass());
        }
        // save target object for later use
        m_targetObject = target;
    }
    public void setValue(Object value, Field field) {
        try {
        // set parameter field value using reflection
            field.set(m_targetObject, value);
            } catch (IllegalAccessException ex) {
            throw new IllegalArgumentException("Field " + field.getName() +
                " is not accessible in object of class " + m_targetObject.getClass().getName());
        }
		}
    public void reportArgumentError(char flag, String text) {
      throw new ArgumentErrorException(text + " for argument '" + flag + "' in argument " + m_currentIndex);
    }
    public static int processArgs(String[] args,
        ParameterDef[] parms, Object target) {
        ArgumentProcessor inst = new ArgumentProcessor(parms, target);
        ...
    }
}

最後に、リスト 6 は int パラメーター値用のパラメーター定義サブクラスの実装の一部を示しています。リスト 6 は、最初に基本クラスを呼び出して、基本クラスの bindToClass() メソッド (リスト 4) をオーバーライドしたのち、フィールドが予想されたタイプと合致しているかをチェックしています。他のパラメーター・タイプ (booleanfloatString など) のサブクラスも同様です。

リスト 6. int パラメーター定義クラス
public class IntDef extends ParameterDef
{
    private int m_min;              // minimum allowed value
    private int m_max;              // maximum allowed value
    public IntDef(char chr, String name, int min, int max) {
        super(chr, name);
        m_min = min;
        m_max = max;
    }
    protected void bindToClass(Class clas) {
        super.bindToClass(clas);
        Class type = m_field.getType();
        if (type != Integer.class && type != Integer.TYPE) {
            throw new IllegalArgumentException("Field '" + m_name +
                "'in " + clas.getName() + " is not of type int");
        }
    }
    public void handle(ArgumentProcessor proc) {
        // set up for validating
        boolean minus = false;
        boolean digits = false;
        int value = 0;
        // convert number supplied in argument list to 'value'
        ...
        // make sure we have a valid value
        value = minus ? -value : value;
        if (!digits) {
            proc.reportArgumentError(m_char, "Missing value");
        } else if (value < m_min || value > m_max) {
            proc.reportArgumentError(m_char, "Value out of range");
        } else {
            proc.setValue(new Integer(value), m_field);
        }
    }
}

ライブラリーを閉じる

この記事で、私はリフレクションの例として、実行時にコマンドライン引数を処理するライブラリーの設計についてざっと説明しました。このライブラリーは、リフレクションを有効に使用する方法の良い例となっています。なぜなら、パフォーマンスを大幅に犠牲にすることなく、アプリケーション・コードを単純化しているためです。では、どれだけのパフォーマンスが犠牲になっているのでしょうか?私が開発したシステムで簡単なテストを行った結果、単純なテスト・プログラムは、引数処理なしで実行したものと比べると、完全なライブラリーを使用した引数処理付きの実行には平均 40ミリ秒長くかかりました。その時間のほとんどは、ライブラリークラスおよびライブラリーによって使用される他のクラスのロードに当てられています。したがって、多くのコマンドライン・パラメーターや多くの引数値をアプリケーションが備えたとしても、これよりもっと長くかかるとは考えにくいです。私のコマンドライン・アプリケーションに関しては、余計にかかった 40ミリ秒はまったく気になるものではありません。

完全なライブラリー・コードは参考文献のリンクから利用可能です。そこにはこの記事で省いた、パラメーター・フラグのフォーマットされたリストを簡単に生成するためのフックや、アプリケーションの使用法に役立つ説明といった細かい点などが含まれています。みなさんのプログラムの中で自由にこのライブラリーを使用して、役に立つと思われる方法で拡張していってください。

このシリーズの第 1 回で Java クラスの基本、第 2 回および第 3 回で Java リフレクション API の法則について取り上げましたので、今後は、バイトコード操作のあまり広まっていないパスについてご紹介していこうと思います。第 4 回は、バイナリー・クラスで動作するユーザー・フレンドリーな Javassist ライブラリーへの考察から始めていきます。メソッドを変形してみたくても、バイトコードでのプログラム作成には気が進まないでしょう?Javassist はあなたのニーズにまさに適したツールであるかもしれません。次回それを確かめてみてください。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=219325
ArticleTitle=Java プログラミングのダイナミックス: 第 3 回 実用的なリフレクション
publish-date=07152003