﹁DI︵依存性注入︶からどこへ行こうか その1﹂において‥
DI︵依存性注入︶については、雑誌や書籍で随分紹介されているので、そういうのを見てください。
こんなこと[注‥DI化]して何がうれしいかって? それは、ファウラー先生とかその他エライ人とかエラクない人とかに聞いてください。 と書きましたが、DI︵Dependency Injection; 依存性注入︶そのものについても説明を試みてみましょう。具体的なサンプルを使うことにします。そのため、サンプルの説明が長くなってしまうのが困ったことですが、まー、単なる能書きよりはサンプルがあったほうがいいでしょ。 内容‥ (一)サンプルはテンプレート処理系 (二)レクサー︵字句処理系︶ (三)レクサーをインターフェース経由で使う (四)サービス・ロケーター (五)依存性が消えてない! (六)DI︵依存性注入︶登場 (七)DIが、かつてIoC︵制御の逆転︶と呼ばれていた理由
こんなこと[注‥DI化]して何がうれしいかって? それは、ファウラー先生とかその他エライ人とかエラクない人とかに聞いてください。 と書きましたが、DI︵Dependency Injection; 依存性注入︶そのものについても説明を試みてみましょう。具体的なサンプルを使うことにします。そのため、サンプルの説明が長くなってしまうのが困ったことですが、まー、単なる能書きよりはサンプルがあったほうがいいでしょ。 内容‥ (一)サンプルはテンプレート処理系 (二)レクサー︵字句処理系︶ (三)レクサーをインターフェース経由で使う (四)サービス・ロケーター (五)依存性が消えてない! (六)DI︵依存性注入︶登場 (七)DIが、かつてIoC︵制御の逆転︶と呼ばれていた理由
●サンプルはテンプレート処理系
またXion処理系︵http://www.chimaira.org/tmp/Xion-0.1.tgz︶を例にしようかとも思ったのですが、サンプルには少し大きい気がするので、別な︵しかしよく似た︶例をでっち上げます。 すごく簡単なテンプレート︵Very Simple Templates; VST︶処理系にしましょう。次がテンプレートの例。テンプレート処理系により、{お客様名}と{来店日}の部分が適切な文字列に置換されることになります。 テンプレート処理は、次の2つの部分に分けて考えます。 (一)テンプレート・テキストをロードしてメモリ内に扱いやすいデータ構造を作る。 (二)実際の置換処理をして結果を出力する。 ロード処理を行うのはVSTパーザーですが、このパーザーはSAX︵Simple API for XML︶を真似て、﹁構文要素ごとに処理メソッド︵ハンドラ︶をコールバックする方式﹂とします。パーザー︵それしか作ってない(苦笑)︶のソースコードは以下のとおり‥パッケージは使ってないし、同一ファイルに複数クラスを詰め込んであり、いたってquick-and-dirtyですが、処理の方針は読み取れるでしょう。 ●VSTLexer.java︵テキスト︶ ●VSTParser.java︵テキスト︶
{お客様名}様、こんにちは。{来店日}にはご来店いただき、
まことにありがとうございます。
本日は{お客様名}様に新商品をご紹介いたします。
…
●レクサー︵字句処理系︶
サンプルにおいて、パーザー︵VSTParser︶はレクサー︵VSTLexer︶を下請けに使います。レクサーは、入力テキストを、'{'︵左波括弧‥left brace︶、'}'︵右波括弧‥right brace︶、その他のテキスト部分に切り分け、ファイルの最後ではEOF︵end-of-file︶を知らせます。レクサーの主要メソッドnextTokenの戻り値は次のVSTToken型です。今回の話題/文脈からは蛇足ですが、次の点を注意しておきます‥ ●特殊記号'{'、'}'が︵適当な方式で︶エスケープされていれば、それはテキストとして返す。 ●その他の文字がエスケープされているときも、エスケープをほどいて︵unescapeして︶返す。 ●レクサーは、'{'、'}'の内部と外部を区別する必要はない。 ●ひとかたまりのテキストを、何個かのテキスト・トークンに分けて返してもよい。 ●トークンのvalueがnullであってはいけないが、""であってもよい。 なお、サンプル・コードでは、'{'のエスケープには'{{'︵波括弧を2個続ける︶を使っています。
/* トークン・データ */
class VSTToken {
enum Kind {L_BRACE, R_BRACE, TEXT, EOF}
final Kind kind;
final String value;
VSTToken(Kind kind, String value) {
this.kind = kind;
this.value = value;
}
}
●レクサーをインターフェース経由で使う
パーザーはレクサーを必要とします。サンプルでは、パーザーの主要メソッドparse内で、次にようにしてレクサーを入手しています。これだと、パーザー︵VSTParser︶は、レクサーの具体的実装クラスであるVSTLexerに直接に依存するので好ましくない、とされます -- この建前の真偽は詮索しないで素直に従うことにしましょう。で、VSTLexerをインターフェースにします。レクサー・オブジェクトを生成した後でもinputを渡せたほうが便利ですから、setInputというセッターもインターフェースに加えました*1。
public void parse(Reader input) throws IOException, VSTParseException {
VSTLexer lexer = new VSTLexer(input);
// ...
}
もとのVSTLexer実装クラスは、VSTLexerStdImplとでも改名しておきましょう。 さて、インターフェースだけでは生成︵new︶ができないので細工が必要です。ファクトリーを使うのがひとつの方法ですね。
import java.io.*;interface VSTLexer {
public void setInput(Reader reader);
public VSTToken nextToken() throws IOException;
}
setInputがあるので、次のようにもできます。
public void parse(Reader input) throws IOException, VSTParseException {
VSTLexerFactory factory = new VSTLexerFactory("some parameter");
VSTLexer lexer = factory.create(input);
// ...
}
// レクサーの生成は、パーザーのコンストラクタ内でもよい
VSTLexerFactory factory = new VSTLexerFactory("some parameter");
VSTLexer lexer = factory.create();
// 後からinputをセットできるからね
lexer.setInput(input);
●サービス・ロケーター
レクサー・ファクトリーよりもっと一般的な方法を考えます。インターフェースや事前に定義された名前を渡すと、適当なオブジェクトを返すような手配師を考えます。そのような手配師をサービス・ロケーター︵service locator︶と呼ぶようです。※これが純正なサービス・ロケーターになっているかどうかは知らない。
public void parse(Reader input) throws IOException, VSTParseException {
VSTLexer lexer = null;
MyServiceLocator locator = MyServiceLocator.getInstance();
try {
lexer = (VSTLexer)locator.getService(VSTLexer.class);
} catch (Exception e) {
// エラー処理
}
lexer.setInput(input);
// ...
}
●依存性が消えてない!
ファクトリーやサービス・ロケーターを使うと、パーザーのコードからレクサー実装クラスへの参照がなくなるので、﹁レクサー実装クラスへの直接的な依存性﹂は確かに解消されます。しかしその代わり、VSTLexerFactoryやMyServiceLocatorに依存してしまいます。 パーザーを書くプログラマは、VSTLexerFactoryやMyServiceLocatorの存在を知っていなければならないし、その使い方の知識が必要です。しかしこれは、﹁パーザーの処理を書く﹂という本来の目的からすれば余計なこと、要らぬ負担を強いています。 さらに悪いことには、VSTLexerFactoryやMyServiceLocatorを使ったパーザーは、当然ながら、VSTLexerFactoryやMyServiceLocatorがない環境ではコンパイルも実行もできません。パーザーがほんとに必要とするのは、適切なレクサー実装であり、産婆役や手配師であるVSTLexerFactoryやMyServiceLocatorじゃないでしょ。なんか本末転倒だよな。●DI︵依存性注入︶登場
●パーザーはレクサー実装クラスに依存したくない。レクサー実装クラスを直接に知ってはいけない。 ●パーザーがほんとに必要としているのはレクサー実装オブジェクトであり、余計な仲介人なんてお呼びでない。 この一見ジレンマの状況を解決する手段は、レクサー実装を入手する仲介人を﹁見知らぬ外部の存在﹂と見なすことです。誰だか知らないが、とにかく親切なダレカサンがレクサー実装を手配して渡してくれるので、その受け口だけ準備します。 もっとも単純な受け口は、レクサーをセットする変数︵フィールド︶です。次のようなオマジナイを書いておくだけ。あるいは、受け口︵外から見ると注入口︶をセッター・メソッドにして‥
class VSTParser {
public VSTLexer lexer; // ここにダレカサンがセットしてくれる
// ....
}
このようにすれば、パーザーコードは純粋にパージング処理を記述しているだけです。受け口︵注入口︶に関するゆるいお約束ごと︵コンベンション︶には従いますが、仲介人の存在を意識せず、レクサー実装を直接参照することもないプレーン/クリーンなコードになりました。
class VSTParser {
private VSTLexer lexer;
// このメソッドをダレカサンが呼んでくれる
public void setLexer(VSTLexer lexer) {
this.lexer = lexer;
}
// ....
}
●DIが、かつてIoC︵制御の逆転︶と呼ばれていた理由
﹁Inversion of Control コンテナと Dependency Injection パターン﹂によれば、DIはかつて、IoC︵Inversion of Control; 制御の逆転︶と呼ばれていたそうです。﹁制御の逆転﹂じゃなんのことだか分からないから、﹁依存性注入﹂にしたとのこと︵それでもなんだか分からないと思うが︶。 確かに、何かが逆転している印象があるのだけど、どうも﹁制御﹂じゃないような… むしろ、﹁責務︵responsibility︶の逆転﹂でIoRのような気がする。で、逆転した責務は何に関する責務かといえば‥ ●自分に必要なモノ一式を入手する責務 実装クラスの選択、オブジェクトの生成と初期化をファクトリーやサービス・ロケーターに任せても、結局、ファクトリーやサービス・ロケーターを利用して必要なオブジェクトを揃えるコードが残っていました。つまり、﹁自分に必要なモノは︵仲介人の手を借りるにしても︶自分自身で入手せよ﹂だったんですね。 それが、﹁必要なモノの入手は、親切なダレカサンが全部やってくれるから任せていい、自分でやらなくていい﹂に変わったので、﹁必要なモノの入手﹂の責務が“中から外へ移っている”、まー逆転していますよね。 とりあえず、考え方としての﹁依存性注入=責務の逆転﹂はこんなことじゃないでしょうか。具体的技法とか支援環境とかは、また色々とあるでしょうが。
VSTLexer lexer = factory.create();
lexer = (VSTLexer)locator.getService(VSTLexer.class);
*1:ポートベース・コンポネントの発想では、setInputはレクサー機能を表すインターフェースの一部というよりむしろ、ポート間のワイヤリング機構だと考えます。