このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

DI(依存性注入)を白紙から説明してみる

「DI(依存性注入)からどこへ行こうか その1」において:

DI(依存性注入)については、雑誌や書籍で随分紹介されているので、そういうのを見てください。



こんなこと[注:DI化]して何がうれしいかって? それは、ファウラー先生とかその他エライ人とかエラクない人とかに聞いてください。

と書きましたが、DI(Dependency Injection; 依存性注入)そのものについても説明を試みてみましょう。具体的なサンプルを使うことにします。そのため、サンプルの説明が長くなってしまうのが困ったことですが、まー、単なる能書きよりはサンプルがあったほうがいいでしょ。

内容:

  1. サンプルはテンプレート処理系
  2. レクサー(字句処理系)
  3. レクサーをインターフェース経由で使う
  4. サービス・ロケーター
  5. 依存性が消えてない!
  6. DI(依存性注入)登場
  7. DIが、かつてIoC(制御の逆転)と呼ばれていた理由

●サンプルはテンプレート処理系

またXion処理系(http://www.chimaira.org/tmp/Xion-0.1.tgz)を例にしようかとも思ったのですが、サンプルには少し大きい気がするので、別な(しかしよく似た)例をでっち上げます。

すごく簡単なテンプレート(Very Simple Templates; VST)処理系にしましょう。次がテンプレートの例。


{お客様名}様、こんにちは。{来店日}にはご来店いただき、
まことにありがとうございます。
本日は{お客様名}様に新商品をご紹介いたします。
テンプレート処理系により、{お客様名}と{来店日}の部分が適切な文字列に置換されることになります。

テンプレート処理は、次の2つの部分に分けて考えます。

  1. テンプレート・テキストをロードしてメモリ内に扱いやすいデータ構造を作る。
  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はレクサー機能を表すインターフェースの一部というよりむしろ、ポート間のワイヤリング機構だと考えます。