Chain
Gang of FourのCOR(Chain of Responsibility)デザインパターンは、一群の操作をモデル化するのによく使われます。各操作をコマンドオブジェクトとしてモデル化し、他のオブジェクトとリンクしてチェーンにする必要があります。これらのコマンドオブジェクトはデータに作用し、チェーンの次のコマンドに制御を渡すべきかどうかを示します。コードに変更を加えなくても操作を簡単に追加/削除できるのが利点です。実行が成功するためには、コマンドの実行時にコマンドラインから渡された引数とサブコマンドを処理コードで利用できなければなりません。
Commons Chainコンポーネントは、アプリケーション内でこのパターンを使用するのに必要なインターフェイスと実装を提供します。APIは明快で、理解するのも難しくはないでしょう。以降では、このコンポーネントを現実の問題に適用することで用法を説明していきます。CLI(Command Line Interface)アプリケーションを開発したり使用したりした経験のある開発者は少なくないと思います。本稿のCommons Chainのサンプルは、HTTP、FTP、PINGという基本的なネットワークプロトコルを使用するためのCLIアプリケーションです(ソースコードについては「in.co.narayanan.commons.chain」パッケージを見てください)。次のリストはいくつかのコマンドを抜粋したものです。
java CommandProcessor ''-user'' admin ''-password'' manager ''-ping'' {host}
java CommandProcessor ''-user'' admin ''-password'' manager
''-ftp'' {host} ''-get'' {path_to_file}
java CommandProcessor ''-user'' admin ''-password'' manager
''-http -get'' {path_to_file}
今回のサンプルでは、-user引数と-password引数を使って、ネットワークコマンドではなくローカルデータベースに対してユーザーの妥当性検査を行います。-get引数は、-ftpと-httpという両マスタコマンドについて、指定のURLのファイルを取得するためのサブコマンドです。ここではCommons Chainの使用例を示すことが狙いなので、この実装にはスケルトンコードしか入っていません。そのため、-pingコマンドの実行時にはコンソールにログメッセージが表示されるだけで、実際にターゲットに接続することはありません。あとは各自で完全なアプリケーションに仕上げてください。その際はCommons Net APIを使用するようお勧めします。
この問題に対してCORパターンを使用すると、デザインが大幅に簡素化されてモジュール性が向上します。
CLIコンテキスト
CLIコマンドの実行時に操作が正常に行われるためには、処理コードで引数とサブコマンドを利用できる必要があります。CommandlineContextクラスとCLICommandクラスはドメインオブジェクトで、これらはコマンドとサブコマンドと引数を表現します。
CLICommandは自分自身への参照を持っています。これはサブコマンドを表現するためのCompositeデザインパターン実装であり、これにより任意の数のコマンドをネストすることが可能になります。CLICommandのインスタンスはuserとpasswordと一緒にCommandlineContextに入れられるので、実行されるCLI操作の全情報を表現します。コンテキストは実行時にチェーン内のコマンドに渡されます。コンテキスト情報を利用したり制御を次のコマンドに渡したりするのは、チェーン内の各コマンドが行います。
このサンプルではCLIの引数をプライベート変数として表現し、ゲッターメソッドを使ってアクセスします。代替策としてContextBase実装を使用する方法もあります。この方法ではJava Beanイントロスペクションを使って属性値を取得し、Mapビューを提供します。したがって、getUserやgetPasswordのような専用ゲッターメソッドをget("user")やget("password")で置き換えることができます。
チェーン内のコマンド
コマンドに必要なコンテキスト情報を表現したら、次にコマンドとチェーンとサブチェーンをモデル化します(後ほどCommons Chain APIでコマンドとチェーンをどのように実行するかを示します)。Pingクラスは-pingのために実行されるコマンドです。このクラスはexecuteメソッドに渡されたコンテキストを使って、コマンドタイプが-pingかどうかをチェックします。コマンドタイプが他のものなら、このメソッドはfalseを返して、チェーン内の次のコマンドに制御を渡す必要があることをChainフレームワークに知らせます。
Ftpクラスはマスタコマンド-ftpを表現します。したがって、ftpのサブコマンドである-getと-lsを処理するために、これはChainBaseのサブクラスとしてモデル化されます。実行メソッドはコマンドタイプをチェックします。それが-ftpなら、このチェーン内のFtpGetコマンドとFtpLsコマンドを実行するために基本クラスのexecuteが呼び出されます。このアプリケーションでは、コマンドに-http -getなどのサブコマンドが含まれていれば、それはChainとしてモデル化され、該当するコマンド制御が委ねられます。
チェーン
このアプリケーションで使われるチェーンとコマンドを視覚的に表現すると、次のようになります。
Main Chain Authenticator (Verifies user and password) --> Ping (Command) --> Ftp (Sub-chain A) --> Default (Prints invalid command message) Sub-chain A FtpGet (Command) --> FtpLs (Command)
チェーンはChainBaseクラスまたはCatalogBaseクラスを使って実行時にプログラム内から作成できます。代替策として、XMLファイル内でコマンドとチェーンを指定し、ConfigParserを使ってチェーンを構築するという方法もあります。今回のサンプルで使用する「catalog.xml」ファイルは以下のとおりです。
<?xml version="1.0" ?> <catalog> <chain name="CommandProcessor"> <command name="authenticate" className="in.co.narayanan.commons.chain.Authenticator"/> <command name="ping" className="in.co.narayanan.commons.chain.Ping"/> <chain name="ftp" className="in.co.narayanan.commons.chain.Ftp"> <command name="get" className="in.co.narayanan.commons.chain.FtpGet"/> <command name="ls" className="in.co.narayanan.commons.chain.FtpLs"/> </chain> <command name="default" className="in.co.narayanan.commons.chain.DefaultCLICommand"/> </chain> </catalog>
フレームワークのセットアップと実行
次のリストはCommons Chainフレームワークをセットアップして実行するためのコードです。
ConfigParser parser = new ConfigParser(); parser.parse(getClass().getResource("catalog.xml")); Catalog catalog = CatalogFactoryBase.getInstance().getCatalog(); .... Command command = catalog.getCommand("CommandProcessor"); command.execute(getPingCommand()); command.execute(getFtpGetCommand()); command.execute(getFtpLsCommand()); command.execute(getInvalidCommand()); command.execute(getInvalidCredentials());
「catalog.xml」ファイルが解析され、結果としてCatalogの参照がCatalogFactoryBaseから取得されます。これで各CLIコマンドの呼び出しの「モックアップ」ができたことになります。完全なコードはテストケースクラスTestChainに収められています。なお、別個のクラスでコマンドライン入力を解析してコンテキストオブジェクトを作成し、それらをチェーンに渡すのが理想的です。その際はCommons CLIを使用することができます。

''-ftp'' {host} ''-get'' {path_to_file}
java CommandProcessor ''-user'' admin ''-password'' manager