レベル: 中級 Dennis Sosnoski (dms@sosnoski.com), President, Sosnoski Software Solutions, Inc.
2003年 7月 15日 コマンド・ライン引数処理は不快な作業のうちの1つであり、過去に何度それを行ってきたとしても巡り続けるように思われます。何度も何度も同じコードのバリエーションを書くのではなく、引数処理のジョブを単純化するためにリフレクションを使用してはどうでしょう?JavaコンサルタントのDennis Sosnoski氏はその方法を解説しています。この記事では、Dennis氏はコマンド・ライン引数でそれ自身を実際に処理するオープン・ソース・ライブラリーについて概説いたします。
前回の記事では、JavaリフレクションAPIを紹介し、そのいくつかの基本的な機能についてざっと説明をしました。またリフレクションの性能についても調査し、リフレクションがアプリケーションにおいて使用されるべき/使用されないべきのガイドラインを示したところで前回は終了しました。今回は、リフレクションの強み・弱みにぴったりと合致するようなアプリケーションを見ていくことで、コマンド・ライン引数処理用のライブラリーについての話を進めていくことにします。
まずは解決すべき問題を定義し、実装コードへと話しが進む前にライブラリーのインタフェースを設計することにします。私が実際にしてきたライブラリー開発は、それほど構造化されていませんでした。--共通のコード・ベースを使用する一群のアプリケーションの既存コードを単純化することから取り掛かり、そこから一般化をしました。この記事での "定義-設計-構築" という順序は、開発プロセスの詳細な説明よりはるかに簡潔です。しかし、ライブラリーを作成する過程において、私は最初の仮定のうちのいくつかを改訂して、ライブラリー・コードを仕上げました。おそらく、これがみなさんのリフレクションベースのアプリケーション開発のモデルとして役に立つことがお分かりになることでしょう。
問題の定義
私はコマンド・ラインからの引数を使って、多くのJavaアプリケーションを作成してきました。たいていのアプリケーションは非常に小さなものから着手したものの、いくつかは当初のプランをはるかに越えて大きくなってしまいました。このプロセスにより私が気づいた点から、標準的なパターンができあがりました。
- 1つまたは2つの必須引数を特定の順番で指定することから始めます。
- アプリケーションで行われるより多くのことを考え、引数を追加していきます。
- 毎回すべての引数を入力するのはうんざりするので、デフォルト値を使うことで、いくつかの引数は省略可能にします。
- 引数の順番は忘れてしまうので、引数指定は任意の順でできるようにコードを変更します。
- 興味を持っている人々にアプリケーションを使ってもらいます。彼らは、どんな引数が必要とされているかを知りません。ですから、引数のエラー・チェックや「ヘルプ」の説明を追加します。
ステップ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回の呼出しで、実際にコマンドライン引数処理を扱うことができますが、アプリケーションにはまずライブラリーに特定のパラメーターを定義する手段が必要です。これらのパラメーターには、いくつかの異なるタイプがあります(事業計画生成の例の場合は、boolean、int、float、java.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()メソッドは、引数値を解釈した後ArgumentProcessorのsetValue()メソッドを呼び出します。リスト5は、コンストラクターおよび,setValue()メソッドで呼出しをバインドしているパラメーターを含むArgumentProcessorクラスの一部です。
リスト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)をオーバーライドしたのち、フィールドが予想されたタイプと合致しているかをチェックしています。他のパラメーター・タイプ(boolean、float、Stringなど)のサブクラスも同様です。
リスト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はあなたのニーズにまさに適したツールであるかもしれません。次回それを確かめてみてください。
参考文献
著者について  | 
|  | Dennis Sosnoskiはシアトル地域にあるJava技術のコンサルティング会社、Sosnoski Software Solutions, Inc.の創立者で、主席コンサルタントでもあり、またXMLやWebサービスに関するトレーニングやコンサルティングの専門家でもあります。彼のプロとしてのソフトウェア開発経験は30年以上に渡り、ここ数年はサーバー側のXML技術やJava技術に注力しています。Dennisは、全米各地で行われる会議で頻繁に講演を行っています。また、Javaクラスワーキング技術を基に構築された、オープンソースの
JiBX XML Data Binding
フレームワークの中心開発者でもあります。
|
記事の評価
|