以下の内容はhttps://m-hiyama.hatenablog.com/entry/20060926/1159253903より取得しました。


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

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

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


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

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

内容:

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

また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はレクサー機能を表すインターフェースの一部というよりむしろ、ポート間のワイヤリング機構だと考えます。




以上の内容はhttps://m-hiyama.hatenablog.com/entry/20060926/1159253903より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14