DI(依存性注入)については、雑誌や書籍で随分紹介されているので、そういうのを見てください。
こんなこと[注: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ですが、処理の方針は読み取れるでしょう。
●レクサー(字句処理系)
サンプルにおいて、パーザー(VSTParser)はレクサー(VSTLexer)を下請けに使います。レクサーは、入力テキストを、'{'(左波括弧;left brace)、'}'(右波括弧;right brace)、その他のテキスト部分に切り分け、ファイルの最後ではEOF(end-of-file)を知らせます。レクサーの主要メソッドnextTokenの戻り値は次のVSTToken型です。
/* トークン・データ */
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;
}
}
今回の話題/文脈からは蛇足ですが、次の点を注意しておきます:
- 特殊記号'{'、'}'が(適当な方式で)エスケープされていれば、それはテキストとして返す。
- その他の文字がエスケープされているときも、エスケープをほどいて(unescapeして)返す。
- レクサーは、'{'、'}'の内部と外部を区別する必要はない。
- ひとかたまりのテキストを、何個かのテキスト・トークンに分けて返してもよい。
- トークンのvalueがnullであってはいけないが、""であってもよい。
なお、サンプル・コードでは、'{'のエスケープには'{{'(波括弧を2個続ける)を使っています。
●レクサーをインターフェース経由で使う
パーザーはレクサーを必要とします。サンプルでは、パーザーの主要メソッドparse内で、次にようにしてレクサーを入手しています。
public void parse(Reader input) throws IOException, VSTParseException {
VSTLexer lexer = new VSTLexer(input);
// ...
}
これだと、パーザー(VSTParser)は、レクサーの具体的実装クラスであるVSTLexerに直接に依存するので好ましくない、とされます -- この建前の真偽は詮索しないで素直に従うことにしましょう。で、VSTLexerをインターフェースにします。レクサー・オブジェクトを生成した後でもinputを渡せたほうが便利ですから、setInputというセッターもインターフェースに加えました*1。
import java.io.*;interface VSTLexer {
public void setInput(Reader reader);
public VSTToken nextToken() throws IOException;
}
もとのVSTLexer実装クラスは、VSTLexerStdImplとでも改名しておきましょう。
さて、インターフェースだけでは生成(new)ができないので細工が必要です。ファクトリーを使うのがひとつの方法ですね。
public void parse(Reader input) throws IOException, VSTParseException {
VSTLexerFactory factory = new VSTLexerFactory("some parameter");
VSTLexer lexer = factory.create(input);
// ...
}
setInputがあるので、次のようにもできます。
// レクサーの生成は、パーザーのコンストラクタ内でもよい
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はレクサー機能を表すインターフェースの一部というよりむしろ、ポート間のワイヤリング機構だと考えます。