制御の反転コンテナと依存性注入パターン

Javaコミュニティでは、異なるプロジェクトのコンポーネントを1つのまとまったアプリケーションに組み立てるのに役立つ軽量コンテナが急増しています。これらのコンテナの基礎となっているのは、ワイヤリングを実行する方法の共通パターンであり、「制御の反転」という非常に一般的な名前で呼ばれる概念です。この記事では、このパターンが「依存性注入」というより具体的な名前でどのように機能するかを掘り下げ、サービスロケータの代替手段と比較します。どちらを選択するかは、設定と使用を分離するという原則ほど重要ではありません。

2004年1月23日



エンタープライズJavaの世界で面白いことの1つは、メインストリームのJ2EEテクノロジーの代替手段を構築する活動が非常に活発であることで、その多くはオープンソースで行われています。この多くは、メインストリームのJ2EEの世界における複雑さに対応したものですが、多くの場合、代替手段を探求し、創造的なアイデアを生み出しています。対処すべき共通の問題は、さまざまな要素をどのように結び付けるかです。異なるチームが互いについてほとんど知識を持たずに構築したWebコントローラーアーキテクチャとデータベースインターフェースバッキングをどのように組み合わせるか。多くのフレームワークがこの問題に取り組んでおり、いくつかのフレームワークは、さまざまなレイヤーのコンポーネントを組み立てるための一般的な機能を提供するために分岐しています。これらは、多くの場合、軽量コンテナと呼ばれ、例としてはPicoContainerSpringなどがあります。

これらのコンテナの根底には、これらの特定のコンテナとJavaプラットフォームの両方を超えた、多くの興味深い設計原則があります。ここでは、これらの原則のいくつかを探求したいと思います。使用する例はJavaですが、私の著作のほとんどと同様に、原則は他のOO環境、特に.NETにも同様に適用できます。

コンポーネントとサービス

要素を結び付けるというトピックは、サービスとコンポーネントという用語を取り巻く厄介な用語の問題にすぐに引きずり込まれます。これらのことの定義に関する、長くて矛盾した記事を簡単に見つけることができます。ここでの私の目的のために、これらの多重定義された用語の現在の使用方法を以下に示します。

コンポーネントとは、コンポーネントの作成者の制御が及ばないアプリケーションによって、変更なしで使用することを目的としたソフトウェアの塊を意味します。「変更なし」とは、使用するアプリケーションがコンポーネントのソースコードを変更しないことを意味しますが、コンポーネントの作成者が許可する方法でコンポーネントを拡張することにより、コンポーネントの動作を変更する場合があります。

サービスは、外部アプリケーションで使用されるという点でコンポーネントに似ています。主な違いは、コンポーネントはローカルで使用されることを期待していることです(jarファイル、アセンブリ、dll、またはソースインポートと考えてください)。サービスは、同期または非同期のいずれかのリモートインターフェースを介してリモートで使用されます(例:Webサービス、メッセージングシステム、RPC、またはソケット)。

この記事では主にサービスを使用していますが、同じロジックの多くはローカルコンポーネントにも適用できます。実際、リモートサービスに簡単にアクセスするには、ある種のローカルコンポーネントフレームワークが必要になることがよくあります。しかし、「コンポーネントまたはサービス」と書くのは、読み書きが面倒であり、サービスは現在、はるかに流行しています。

単純な例

これらすべてをより具体的にするために、これらすべてについて説明するための実行例を使用します。私のすべての例と同様に、これは超単純な例の1つです。現実的ではないほど小さいですが、実際の例にはまり込むことなく、何が起こっているかを視覚化するのに十分なものになることを願っています。

この例では、特定の監督が監督した映画のリストを提供するコンポーネントを作成しています。この非常に便利な関数は、単一のメソッドによって実装されます。

クラスMovieLister ...

  public Movie[] moviesDirectedBy(String arg) {
      List allMovies = finder.findAll();
      for (Iterator it = allMovies.iterator(); it.hasNext();) {
          Movie movie = (Movie) it.next();
          if (!movie.getDirector().equals(arg)) it.remove();
      }
      return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
  }

この関数の 実装は非常に単純で、ファインダーオブジェクト(後で説明します)に、知っているすべての映画を返すように要求します。次に、このリストを調べて、特定の監督が監督した映画を返します。 この特定の単純さは修正しません。この記事の要点の足場になっているからです。

この記事の本当のポイントは、このファインダーオブジェクト、特にlisterオブジェクトを特定のファインダーオブジェクトにどのように接続するかです。これが興味深い理由は、素晴らしい`moviesDirectedBy`メソッドを、すべての映画がどのように保存されているかとは完全に独立させたいからです。したがって、メソッドが行うことはファインダーを参照することだけであり、ファインダーが行うことは`findAll`メソッドに応答する方法を知ることだけです。ファインダーのインターフェースを定義することで、これを明らかにすることができます。

public interface MovieFinder {
    List findAll();
}

さて、これらはすべて非常によく分離されていますが、いつか映画を実際に考え出す具体的なクラスを考え出す必要があります。この場合、このコードをlisterクラスのコンストラクターに配置します。

クラスMovieLister ...

  private MovieFinder finder;
  public MovieLister() {
    finder = new ColonDelimitedMovieFinder("movies1.txt");
  }

実装クラスの名前は、コロン区切りのテキストファイルからリストを取得しているという事実から来ています。詳細は省略しますが、重要なのは、何らかの実装があるということです。

さて、このクラスを自分だけで使用している場合は、すべて問題ありません。しかし、私の友人がこの素晴らしい機能への欲求に圧倒され、私のプログラムのコピーを欲しがったらどうなるでしょうか?彼らも映画のリストを「movies1.txt」というコロン区切りのテキストファイルに保存している場合、すべてが素晴らしいです。映画ファイルの名前が異なる場合は、ファイルの名前をプロパティファイルに簡単に配置できます。しかし、映画のリストを保存する形式がまったく異なる場合はどうでしょうか。SQLデータベース、XMLファイル、Webサービス、または別の形式のテキストファイル?この場合、そのデータを取得するには別のクラスが必要です。`MovieFinder`インターフェースを定義したので、これは`moviesDirectedBy`メソッドを変更しません。しかし、適切なファインダー実装のインスタンスを適切な場所に配置する方法が必要です。

図1:listerクラスでの単純な作成を使用した依存関係

図1は、この状況の依存関係を示しています。`MovieLister`クラスは、`MovieFinder`インターフェースと実装の両方に依存しています。インターフェースのみに依存するようにしたいのですが、それではどのように連携するインスタンスを作成するのでしょうか?

私の著書P of EAAでは、この状況をプラグインとして説明しました。ファインダーの実装クラスは、コンパイル時にプログラムにリンクされていません。これは、友人が何を使用するかわからないためです。代わりに、listerが任意の実装で動作し、その実装が後で私の手から離れてプラグインされるようにしたいと考えています。問題は、listerクラスが実装クラスを認識せずに、それでもインスタンスと通信して作業を行うことができるように、そのリンクをどのように作成できるかということです。

これを実際のシステムに拡張すると、数十のそのようなサービスとコンポーネントが存在する可能性があります。いずれの場合も、インターフェースを介してこれらのコンポーネントと通信することにより、これらのコンポーネントの使用を抽象化できます(コンポーネントがインターフェースを念頭に置いて設計されていない場合は、アダプターを使用します)。しかし、このシステムをさまざまな方法でデプロイする場合、これらのサービスとの相互作用を処理するためにプラグインを使用して、異なるデプロイで異なる実装を使用できるようにする必要があります。

したがって、中心的な問題は、これらのプラグインをアプリケーションにどのように組み立てるかです。これは、この新しいタイプの軽量コンテナが直面する主要な問題の1つであり、普遍的にすべてが制御の反転を使用してそれを行います。

制御の反転

これらのコンテナが「制御の反転」を実装しているため非常に便利であると語っているとき、私は非常に困惑します。制御の反転はフレームワークの共通の特性であるため、これらの軽量コンテナは制御の反転を使用しているため特別であると言うことは、私の車が車輪を持っているため特別であると言うようなものです。

問題は、「彼らは何の制御の側面を反転させているのか?」です。私が最初に制御の反転に遭遇したのは、ユーザーインターフェースの主要な制御でした。初期のユーザーインターフェースは、アプリケーションプログラムによって制御されていました。「名前を入力してください」、「住所を入力してください」などの一連のコマンドがありました。プログラムはプロンプトを駆動し、それぞれに応答を取得します。グラフィカル(または画面ベースの)UIでは、UIフレームワークにこのメインループが含まれ、プログラムは代わりに画面上のさまざまなフィールドのイベントハンドラーを提供します。プログラムの主要な制御は反転され、あなたからフレームワークに移されました。

この新しいタイプのコンテナの場合、反転はプラグイン実装をどのように検索するかについてです。単純な例では、listerはファインダー実装を直接インスタンス化することによって検索しました。これは、ファインダーがプラグインになるのを阻止します。これらのコンテナが使用するアプローチは、プラグインのユーザーが、別のアセンブラーモジュールが実装をlisterに挿入できるようにする規則に従うことを保証することです。

結果として、このパターンにはより具体的な名前が必要だと思います。制御の反転はあまりにも一般的な用語であるため、人々は混乱します。その結果、さまざまなIoC支持者との多くの議論を経て、私たちは*依存性注入*という名前に落ち着きました。

まず、依存性注入のさまざまな形式について説明することから始めますが、それがアプリケーションクラスからプラグイン実装への依存関係を削除する唯一の方法ではないことを今指摘します。これを行うために使用できる他のパターンはサービスロケータであり、依存性注入の説明が終わった後に説明します。

依存性注入の形式

依存性注入の基本的な考え方は、別個のオブジェクトであるアセンブラーを用意して、listerクラスのフィールドにファインダーインターフェースの適切な実装を設定することです。図2のような依存関係図になります。

図2:依存性注入器の依存関係

依存性注入には、主に3つのスタイルがあります。私がそれらに使用している名前は、コンストラクタ注入、セッター注入、およびインターフェース注入です。制御の反転に関する現在の議論でこれらについて読むと、これらはタイプ1 IoC(インターフェース注入)、タイプ2 IoC(セッター注入)、タイプ3 IoC(コンストラクタ注入)と呼ばれます。数値名は覚えにくいので、ここでは私が使用している名前を使用しました。

PicoContainerを用いたコンストラクタ注入

PicoContainerと呼ばれる軽量コンテナを使用して、この注入がどのように行われるかを示すことから始めます。私がここで始めているのは、主にThoughtworksの同僚の何人かがPicoContainerの開発に非常に積極的であるためです(そうです、これは一種の企業縁故主義です)。

PicoContainerは、コンストラクタを使用して、ファインダー実装をlisterクラスにどのようにインジェクトするかを決定します。これが機能するためには、ムービーlisterクラスは、インジェクトされる必要があるすべてを含むコンストラクタを宣言する必要があります。

クラスMovieLister ...

  public MovieLister(MovieFinder finder) {
      this.finder = finder;       
  }

ファインダー自体もpicoコンテナによって管理されるため、テキストファイルのファイル名がコンテナによってファインダーにインジェクトされます。

class ColonMovieFinder...

  public ColonMovieFinder(String filename) {
      this.filename = filename;
  }

次に、picoコンテナには、各インターフェースに関連付ける実装クラスと、ファインダーにインジェクトする文字列を指示する必要があります。

private MutablePicoContainer configureContainer() {
    MutablePicoContainer pico = new DefaultPicoContainer();
    Parameter[] finderParams =  {new ConstantParameter("movies1.txt")};
    pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
    pico.registerComponentImplementation(MovieLister.class);
    return pico;
}

この設定コードは、通常、別のクラスで設定されます。この例では、私のlisterを使用する各友人は、独自のセットアップクラスに適切な設定コードを記述する可能性があります。もちろん、このような設定情報は個別の設定ファイルに保持するのが一般的です。設定ファイルを読み取り、コンテナを適切に設定するクラスを記述できます。PicoContainer自体にはこの機能は含まれていませんが、NanoContainerと呼ばれる密接に関連するプロジェクトがあり、XML設定ファイルを使用できるように適切なラッパーを提供しています。このようなナノコンテナはXMLを解析し、基礎となるpicoコンテナを設定します。プロジェクトの理念は、設定ファイル形式を基礎となるメカニズムから分離することです。

コンテナを使用するには、次のようなコードを記述します。

public void testWithPico() {
    MutablePicoContainer pico = configureContainer();
    MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);
    Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
    assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}

この例ではコンストラクタインジェクションを使用しましたが、PicoContainerはセッターインジェクションもサポートしていますが、開発者はコンストラクタインジェクションを好んでいます。

Springを用いたセッター注入

Springフレームワークは、エンタープライズJava開発のための広範なフレームワークです。トランザクション、永続性フレームワーク、Webアプリケーション開発、JDBCの抽象化レイヤーが含まれています。PicoContainerと同様に、コンストラクタインジェクションとセッターインジェクションの両方をサポートしていますが、開発者はセッターインジェクションを好む傾向があります。そのため、この例では適切な選択です。

ムービーlisterがインジェクションを受け入れるようにするには、そのサービスのセッティングメソッドを定義します

クラスMovieLister ...

  private MovieFinder finder;
public void setFinder(MovieFinder finder) {
  this.finder = finder;
}

同様に、ファイル名のセッターを定義します。

class ColonMovieFinder...

  public void setFilename(String filename) {
      this.filename = filename;
  }

3番目のステップは、ファイルの設定を行うことです。Springは、XMLファイルとコードによる設定をサポートしていますが、XMLが想定される方法です。

<beans>
    <bean id="MovieLister" class="spring.MovieLister">
        <property name="finder">
            <ref local="MovieFinder"/>
        </property>
    </bean>
    <bean id="MovieFinder" class="spring.ColonMovieFinder">
        <property name="filename">
            <value>movies1.txt</value>
        </property>
    </bean>
</beans>

テストは次のようになります。

public void testWithSpring() throws Exception {
    ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");
    MovieLister lister = (MovieLister) ctx.getBean("MovieLister");
    Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
    assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}

インターフェース注入

3番目のインジェクション手法は、インジェクション用のインターフェースを定義して使用することです。Avalonは、この手法を場所で使用するフレームワークの例です。それについては後で詳しく説明しますが、この場合は、簡単なサンプルコードで使用します。

この手法では、インジェクションを実行するために使用するインターフェースを定義することから始めます。ムービーファインダーをオブジェクトにインジェクトするためのインターフェースを次に示します。

public interface InjectFinder {
    void injectFinder(MovieFinder finder);
}

このインターフェースは、MovieFinderインターフェースを提供する人によって定義されます。listerなど、ファインダーを使用するすべてのクラスによって実装される必要があります。

class MovieLister implements InjectFinder

  public void injectFinder(MovieFinder finder) {
      this.finder = finder;
  }

同様のアプローチを使用して、ファイル名をファインダー実装にインジェクトします。

public interface InjectFinderFilename {
    void injectFilename (String filename);
}

class ColonMovieFinder implements MovieFinder, InjectFinderFilename...

  public void injectFilename(String filename) {
      this.filename = filename;
  }

次に、いつものように、実装を接続するための設定コードが必要です。簡単にするために、コードで実行します。

class Tester...

  private Container container;

   private void configureContainer() {
     container = new Container();
     registerComponents();
     registerInjectors();
     container.start();
  }

この設定には2つの段階があります。ルックアップキーを介したコンポーネントの登録は、他の例と非常によく似ています。

class Tester...

  private void registerComponents() {
    container.registerComponent("MovieLister", MovieLister.class);
    container.registerComponent("MovieFinder", ColonMovieFinder.class);
  }

新しい手順は、依存コンポーネントをインジェクトするインジェクターを登録することです。各インジェクションインターフェースには、依存オブジェクトをインジェクトするためのコードが必要です。ここでは、インジェクターオブジェクトをコンテナに登録することでこれを行います。各インジェクターオブジェクトは、インジェクターインターフェースを実装します。

class Tester...

  private void registerInjectors() {
    container.registerInjector(InjectFinder.class, container.lookup("MovieFinder"));
    container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector());
  }
public interface Injector {
  public void inject(Object target);

}

依存関係がこのコンテナ用に記述されたクラスである場合、ムービーファインダーで行うように、コンポーネント自体がインジェクターインターフェースを実装するのが理にかなっています。文字列などの汎用クラスの場合、設定コード内で内部クラスを使用します。

class ColonMovieFinder implements Injector...

  public void inject(Object target) {
    ((InjectFinder) target).injectFinder(this);        
  }

class Tester...

  public static class FinderFilenameInjector implements Injector {
    public void inject(Object target) {
      ((InjectFinderFilename)target).injectFilename("movies1.txt");      
    }
    }

次に、テストでコンテナを使用します。

class Tester…

  public void testIface() {
    configureContainer();
    MovieLister lister = (MovieLister)container.lookup("MovieLister");
    Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
    assertEquals("Once Upon a Time in the West", movies[0].getTitle());
  }

コンテナは、宣言されたインジェクションインターフェースを使用して依存関係を把握し、インジェクターを使用して正しい依存関係をインジェクトします。(ここで行った特定のコンテナ実装は、この手法にとって重要ではなく、笑ってしまうだけなので表示しません。)

サービスロケータの使用

依存性注入の主な利点は、MovieListerクラスが具体的なMovieFinder実装に依存していることを排除することです。これにより、listerを友人に提供し、彼らが自分の環境に適した実装をプラグインすることができます。インジェクションがこの依存関係を解消する唯一の方法ではありません。もう1つは、サービスロケーターを使用することです。

サービスロケーターの背後にある基本的な考え方は、アプリケーションが必要とする可能性のあるすべてのサービスを取得する方法を知っているオブジェクトを持つことです。そのため、このアプリケーションのサービスロケーターには、必要なときにムービーファインダーを返すメソッドがあります。もちろん、これは負担を少しだけ軽減します。ロケーターをlisterに入れる必要があり、図3の依存関係が生じます

図3:サービスロケーターの依存関係

この場合、ServiceLocatorをシングルトンレジストリとして使用します。listerは、インスタンス化されたときにそれを使用してファインダーを取得できます。

クラスMovieLister ...

  MovieFinder finder = ServiceLocator.movieFinder();

class ServiceLocator...

  public static MovieFinder movieFinder() {
      return soleInstance.movieFinder;
  }
  private static ServiceLocator soleInstance;
  private MovieFinder movieFinder;

インジェクションアプローチと同様に、サービスロケーターを設定する必要があります。ここではコードで実行していますが、設定ファイルから適切なデータを読み取るメカニズムを使用するのは難しくありません。

class Tester...

  private void configure() {
      ServiceLocator.load(new ServiceLocator(new ColonMovieFinder("movies1.txt")));
  }

class ServiceLocator...

  public static void load(ServiceLocator arg) {
      soleInstance = arg;
  }

  public ServiceLocator(MovieFinder movieFinder) {
      this.movieFinder = movieFinder;
  }

テストコードは次のとおりです。

class Tester...

  public void testSimple() {
      configure();
      MovieLister lister = new MovieLister();
      Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
      assertEquals("Once Upon a Time in the West", movies[0].getTitle());
  }

私は、これらの種類のサービスロケーターは、実装を代用できないためテストできないため、悪いことであるという不満をよく耳にします。確かに、この種の問題に陥るように設計することはできますが、そうする必要はありません。この場合、サービスロケーターインスタンスは単純なデータホルダーです。サービスのテスト実装を使用してロケーターを簡単に作成できます。

より洗練されたロケーターの場合、サービスロケーターをサブクラス化し、そのサブクラスをレジストリのクラス変数に渡すことができます。インスタンス変数に直接アクセスするのではなく、インスタンスのメソッドを呼び出すように静的メソッドを変更できます。スレッド固有のストレージを使用して、スレッド固有のロケーターを提供できます。これはすべて、サービスロケーターのクライアントを変更せずに実行できます。

これを考える1つの方法は、サービスロケーターはレジストリであり、シングルトンではないということです。シングルトンはレジストリを実装する簡単な方法を提供しますが、その実装の決定は簡単に変更できます。

ロケータのための分離インターフェースの使用

上記の単純なアプローチの問題の1つは、MovieListerが1つのサービスしか使用していない場合でも、完全なサービスロケータークラスに依存していることです。ロールインターフェースを使用することで、これを削減できます。そうすれば、完全なサービスロケーターインターフェースを使用する代わりに、listerは必要なインターフェースのビットだけを宣言できます。

この状況では、listerのプロバイダーは、ファインダーを取得するために必要なロケーターインターフェースも提供します。

public interface MovieFinderLocator {
    public MovieFinder movieFinder();

ロケーターは、ファインダーへのアクセスを提供するために、このインターフェースを実装する必要があります。

MovieFinderLocator locator = ServiceLocator.locator();
MovieFinder finder = locator.movieFinder();
public static ServiceLocator locator() {
     return soleInstance;
 }
 public MovieFinder movieFinder() {
     return movieFinder;
 }
 private static ServiceLocator soleInstance;
 private MovieFinder movieFinder;

インターフェースを使用したいので、静的メソッドを介してサービスにアクセスすることはできなくなりました。クラスを使用してロケーターインスタンスを取得し、それを使用して必要なものを取得する必要があります。

動的サービスロケータ

上記の例は静的でした。つまり、サービスロケータークラスには、必要な各サービスのメソッドがあります。これが唯一の方法ではありません。必要なサービスをすべて格納し、実行時に選択できる動的サービスロケーターを作成することもできます。

この場合、サービスロケーターは各サービスのフィールドの代わりにマップを使用し、サービスを取得およびロードするための汎用メソッドを提供します。

class ServiceLocator...

  private static ServiceLocator soleInstance;
  public static void load(ServiceLocator arg) {
      soleInstance = arg;
  }
  private Map services = new HashMap();
  public static Object getService(String key){
      return soleInstance.services.get(key);
  }
  public void loadService (String key, Object service) {
      services.put(key, service);
  }

設定には、適切なキーを持つサービスのロードが含まれます。

class Tester...

  private void configure() {
      ServiceLocator locator = new ServiceLocator();
      locator.loadService("MovieFinder", new ColonMovieFinder("movies1.txt"));
      ServiceLocator.load(locator);
  }

同じキー文字列を使用してサービスを使用します。

クラスMovieLister ...

  MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");

全体として、私はこのアプローチが好きではありません。確かに柔軟性がありますが、それほど明確ではありません。サービスに到達する方法を見つける唯一の方法は、テキストキーを使用することです。明示的なメソッドの方が、インターフェース定義を見ることでどこにあるかを見つけやすいので、お勧めします。

Avalonを用いたロケータと注入の併用

依存性注入とサービスロケーターは、必ずしも相互に排他的な概念ではありません。両方を一緒に使用する良い例は、Avalonフレームワークです。Avalonはサービスロケーターを使用しますが、インジェクションを使用してコンポーネントにロケーターの場所を指示します。

Berin Loritschは、Avalonを使用して実行中の例のこの簡単なバージョンを送信してくれました。

public class MyMovieLister implements MovieLister, Serviceable {
    private MovieFinder finder;

    public void service( ServiceManager manager ) throws ServiceException {
        finder = (MovieFinder)manager.lookup("finder");
    } 
      

serviceメソッドはインターフェースインジェクションの例であり、コンテナがサービスマネージャーをMyMovieListerにインジェクトできるようにします。サービスマネージャーは、サービスロケーターの例です。この例では、listerはマネージャーをフィールドに格納せず、代わりにすぐにそれを使用してファインダーをルックアップし、それを格納します。

どのオプションを使用するかを決める

これまでのところ、私はこれらのパターンとそのバリエーションをどのように見ているかを説明することに集中してきました。これで、どのパターンをいつ使用するかを判断するのに役立つ、それらの長所と短所について話し始めることができます。

サービスロケータ vs 依存性注入

基本的な選択は、サービスロケーターと依存性注入の間です。最初のポイントは、どちらの実装も、単純な例に欠けている基本的な分離を提供することです。どちらの場合も、アプリケーションコードはサービスインターフェースの具体的な実装から独立しています。2つのパターンの重要な違いは、その実装がアプリケーションクラスにどのように提供されるかです。サービスロケーターを使用すると、アプリケーションクラスはロケーターへのメッセージによって明示的にそれを要求します。インジェクションでは明示的な要求はなく、サービスはアプリケーションクラスに表示されます。したがって、制御の反転です。

制御の反転はフレームワークの一般的な機能ですが、代償を伴うものです。理解するのが難しく、デバッグしようとしているときに問題が発生する傾向があります。したがって、全体として、必要な場合を除いて、それを避けることをお勧めします。これは悪いことだと言っているのではありません。もっと簡単な代替案よりも正当化する必要があると思うだけです。

重要な違いは、サービスロケーターを使用すると、サービスのすべてのユーザーがロケーターに依存することです。ロケーターは他の実装への依存関係を隠すことができますが、ロケーターを表示する必要があります。したがって、ロケーターとインジェクターのどちらを選択するかは、その依存関係が問題かどうかによって異なります。

依存性注入を使用すると、コンポーネントの依存関係を簡単に確認できます。依存性注入を使用すると、コンストラクターなどの注入メカニズムを見て、依存関係を確認できます。サービスロケーターを使用すると、ロケーターの呼び出しのためにソースコードを検索する必要があります。参照の検索機能を備えた最新のIDEはこれを容易にしますが、コンストラクターまたは設定メソッドを見るほど簡単ではありません。

サービス利用者の性質によって、多くの部分が左右されます。多様なクラスがサービスを利用するアプリケーションを構築する場合、アプリケーションクラスからロケーターへの依存は大きな問題ではありません。私が友人にMovie Listerを提供する例では、サービスロケーターを使用すると非常にうまく機能します。必要なのは、設定コードまたは設定ファイルを通じて、適切なサービス実装をフックするようにロケーターを設定することだけです。この種のシナリオでは、インジェクターの反転が何か魅力的なものを提供しているとは思いません。

違いが生じるのは、他の人が書いているアプリケーションに提供するコンポーネントとしてlisterを提供する場合です。この場合、顧客が使用するサービスロケーターのAPIについてはよくわかりません。顧客ごとに互換性のない独自のサービスロケーターを持っている可能性があります。分離インターフェースを使用することで、これを回避できます。各顧客は、自分のインターフェースをロケーターに一致させるアダプターを作成できますが、いずれの場合でも、特定のインターフェースを検索するには最初のロケーターを参照する必要があります。そして、アダプターが登場すると、ロケーターへの直接接続のシンプルさが失われ始めます。

インジェクターを使用すると、コンポーネントからインジェクターへの依存関係がないため、コンポーネントは設定後にインジェクターから追加のサービスを取得できません。

依存性注入を好む理由としてよく挙げられるのは、テストが容易になることです。ここでのポイントは、テストを行うには、実際のサービス実装をスタブまたはモックで簡単に置き換える必要があるということです。しかし、依存性注入とサービスロケーターの間には実際には違いはありません。どちらもスタブ化に非常に適しています。この観察は、サービスロケーターを簡単に置き換えることができるように努力していないプロジェクトから来ているのではないかと思います。これは継続的なテストが役立つところです。テストのためにサービスを簡単にスタブ化できない場合、これは設計に深刻な問題があることを意味します。

もちろん、テストの問題は、JavaのEJBフレームワークなど、非常に侵入的なコンポーネント環境によって悪化します。私の見解では、これらの種類のフレームワークはアプリケーションコードへの影響を最小限に抑えるべきであり、特に編集実行サイクルを遅くするようなことはすべきではありません。重量級コンポーネントを置き換えるためにプラグインを使用すると、テスト駆動開発などのプラクティスに不可欠なこのプロセスに大いに役立ちます。

そのため、主な問題は、作成者が制御できないアプリケーションで使用されることを想定してコードを作成する人々にとってです。これらの場合、サービスロケーターに関する最小限の仮定でさえ問題になります。

コンストラクタ注入 vs セッター注入

サービスの組み合わせについては、常に何らかの規則に従って接続する必要があります。注入の利点は、主に非常に単純な規則を必要とすることです。少なくともコンストラクターとセッターの注入についてはそうです。コンポーネントで特別なことをする必要はなく、インジェクターがすべてを設定するのは非常に簡単です。

インターフェースインジェクションは、すべてを整理するために多くのインターフェースを作成する必要があるため、より侵襲的です。Avalonのアプローチのように、コンテナに必要なインターフェースのセットが小さい場合は、それほど悪くはありません。しかし、コンポーネントと依存関係を組み立てるには多くの作業が必要になるため、軽量コンテナの現在の作物はセッターとコンストラクターのインジェクションを使用しています。

セッターインジェクションとコンストラクターインジェクションの選択は興味深いものです。これは、オブジェクト指向プログラミングのより一般的な問題、つまりコンストラクターでフィールドを埋めるか、セッターでフィールドを埋めるかを反映しているためです。

オブジェクトに関する私の長年のデフォルトは、可能な限り、構築時に有効なオブジェクトを作成することです。このアドバイスは、Kent BeckのSmalltalkベストプラクティスパターン:コンストラクターメソッドとコンストラクターパラメーターメソッドにまでさかのぼります。パラメーター付きのコンストラクターは、有効なオブジェクトを作成することの意味を明確な場所で明確に示しています。複数の方法がある場合は、異なる組み合わせを示す複数のコンストラクターを作成します。

コンストラクター初期化のもう1つの利点は、セッターを提供しないだけで、不変のフィールドを明確に隠すことができることです。これは重要だと思います。何かを変更すべきでない場合、セッターがないことでこれが非常によく伝わります。初期化にセッターを使用すると、これは面倒になる可能性があります。(実際、このような状況では、通常のセッティング規則を避け、`initFoo`のようなメソッドを使用して、出生時にのみ行うべきことを強調することを好みます。)

しかし、どのような状況でも例外があります。コンストラクターパラメーターが多いと、特にキーワードパラメーターのない言語では、物事が乱雑に見える可能性があります。長いコンストラクターは、分割する必要がある過度にビジーなオブジェクトの兆候であることがよくありますが、必要な場合もあります。

有効なオブジェクトを構築する複数の方法がある場合、コンストラクターはパラメーターの数と型のみを変えることができるため、コンストラクターでこれを示すのは難しい場合があります。これはファクトリーメソッドが活躍するときです。これらは、プライベートコンストラクターとセッターの組み合わせを使用して作業を実装できます。コンポーネントアセンブリの従来のファクトリーメソッドの問題は、通常静的メソッドと見なされ、インターフェースにそれらを含めることができないことです。ファクトリークラスを作成できますが、それは別のサービスインスタンスになるだけです。ファクトリーサービスはしばしば良い戦術ですが、それでもここで説明したテクニックの1つを使用してファクトリーをインスタンス化する必要があります。

コンストラクターは、文字列などの単純なパラメーターがある場合にも問題が発生します。セッターインジェクションを使用すると、各セッターに名前を付けて、文字列が何をするかを示すことができます。コンストラクターでは、位置にのみ依存しているため、追跡が困難です。

複数のコンストラクターと継承がある場合、事態は特に厄介になる可能性があります。すべてを初期化するには、各スーパークラスコンストラクターに転送するコンストラクターを提供すると同時に、独自の引数を追加する必要があります。これは、コンストラクターのさらに大きな爆発につながる可能性があります。

欠点はあるものの、私の好みはコンストラクターインジェクションから始めることですが、上記で概説した問題が発生し始めたらすぐにセッターインジェクションに切り替える準備をしてください。

この問題は、フレームワークの一部として依存性注入器を提供するさまざまなチーム間で多くの議論を引き起こしました。しかし、これらのフレームワークを構築するほとんどの人は、どちらか一方を優先する場合でも、両方のメカニズムをサポートすることが重要であることに気付いているようです。

コードまたは設定ファイル

別の、しかししばしば混同される問題は、サービスを接続するためにAPIで設定ファイルを使用するかコードを使用するかです。多くの場所にデプロイされる可能性のあるほとんどのアプリケーションでは、通常、個別の設定ファイルが最も理にかなっています。ほとんどの場合、これはXMLファイルになり、これは理にかなっています。ただし、プログラムコードを使用してアセンブリを行う方が簡単な場合があります。1つのケースは、デプロイメントのバリエーションがあまりない単純なアプリケーションがある場合です。この場合、少しのコードは個別のXMLファイルよりも明確になります。

対照的なケースは、条件付きの手順を含む、アセンブリが非常に複雑な場合です。プログラミング言語に近づき始めると、XMLは機能しなくなり、明確なプログラムを作成するためのすべての構文を備えた実際の言語を使用する方が適切になります。次に、アセンブリを実行するビルダークラスを作成します。明確なビルダーシナリオがある場合は、複数のビルダークラスを提供し、単純な設定ファイルを使用してそれらの間で選択できます。

人々は設定ファイルを定義することに熱心すぎると思います。多くの場合、プログラミング言語は簡単で強力な設定メカニズムを作成します。最新の言語は、大規模システムのプラグインを組み立てるために使用できる小さなアセンブラーを簡単にコンパイルできます。コンパイルが面倒な場合は、うまく機能するスクリプト言語もあります。

設定ファイルは、プログラマー以外の人が編集する必要があるため、プログラミング言語を使用すべきではないとよく言われます。しかし、これはどれくらいの頻度で発生するのでしょうか?複雑なサーバーサイドアプリケーションのトランザクション分離レベルをプログラマー以外の人が変更することを本当に期待しているのでしょうか?言語以外の設定ファイルは、単純な場合にのみうまく機能します。複雑になった場合は、適切なプログラミング言語の使用を検討する時期です。

現在Javaの世界で見られることの1つは、設定ファイルの不協和音です。すべてのコンポーネントには、他のすべてのコンポーネントとは異なる独自の設定ファイルがあります。これらのコンポーネントを12個使用すると、同期を維持するために12個の設定ファイルが簡単に作成されます。

ここでの私のアドバイスは、常にプログラムによるインターフェースですべての設定を簡単に行う方法を提供し、次に個別の設定ファイルを追加機能として扱うことです。プログラムによるインターフェースを使用するように設定ファイル処理を簡単に構築できます。コンポーネントを作成している場合は、プログラムによるインターフェース、設定ファイル形式を使用するか、独自のカスタム設定ファイル形式を作成してプログラムによるインターフェースに関連付けるかをユーザーに任せます。

設定と使用の分離

このすべてにおける重要な問題は、サービスの設定をその使用から分離することです。実際、これは、インターフェースと実装の分離に伴う基本的な設計原則です。これは、条件付きロジックがインスタンス化するクラスを決定し、その条件の将来の評価が重複した条件付きコードではなくポリモーフィズムを通じて行われる場合、オブジェクト指向プログラム内で確認されます。

この分離が単一のコードベース内で有用であるならば、コンポーネントやサービスのような外部要素を使用する場合には特に重要です。最初の問題は、実装クラスの選択を特定のデプロイメントに委ねるかどうかです。もしそうであれば、何らかのプラグインの実装を使用する必要があります。プラグインを使用する場合、プラグインのアセンブリはアプリケーションの残りの部分とは別に実行することが不可欠です。これにより、異なるデプロイメントに対して異なる構成を簡単に置き換えることができます。どのようにこれを実現するかは二次的な問題です。この構成メカニズムは、サービスロケーターを構成することも、インジェクションを使用してオブジェクトを直接構成することもできます。

その他の問題

この記事では、依存性注入とサービスロケーターを使用したサービス構成の基本的な問題に焦点を当てました。これ以外にも注目に値するトピックがいくつかありますが、まだ掘り下げる時間がありませんでした。特に、ライフサイクルの動作の問題があります。一部のコンポーネントには、たとえば停止や開始など、明確なライフサイクルイベントがあります。もう1つの問題は、これらのコンテナでアスペクト指向のアイデアを使用することに対する関心の高まりです。現時点ではこの記事でこの資料を検討していませんが、この記事を拡張するか、別の記事を書くことで、これについてもっと書きたいと思っています。

軽量コンテナ専用のWebサイトを見ることで、これらのアイデアについてさらに詳しく知ることができます。picocontainerspring のWebサイトからサーフィンすると、これらの問題に関するより多くの議論と、さらにいくつかの問題の始まりにつながるでしょう。

結論

現在の軽量コンテナのラッシュはすべて、サービスアセンブリを行う方法に共通の基本的なパターン、つまり依存性注入パターンを持っています。依存性注入は、サービスロケーターに代わる有用な方法です。アプリケーションクラスを構築する場合、この2つはほぼ同等ですが、サービスロケーターは、より単純な動作をするため、わずかに優位性があると思います。ただし、複数のアプリケーションで使用されるクラスを構築する場合は、依存性注入の方が適しています。

依存性注入を使用する場合、選択できるスタイルがいくつかあります。コンストラクタインジェクションを使用することをお勧めします。ただし、このアプローチに固有の問題が発生した場合は、セッターインジェクションに切り替えてください。コンテナを構築または取得することを選択する場合は、コンストラクタインジェクションとセッターインジェクションの両方をサポートするコンテナを探してください。

サービスロケーターと依存性注入のどちらを選択するかは、サービス構成とアプリケーション内でのサービスの使用を分離するという原則ほど重要ではありません。


謝辞

この記事を執筆するにあたって、多くの方々からご協力をいただきました。Rod Johnson、Paul Hammant、Joe Walnes、Aslak Hellesøy、Jon Tirsén、Bill Caputoの各氏は、これらの概念を理解し、この記事の初期の草稿についてコメントしてくれました。Berin LoritschとHamilton Verissimo de Oliveiraは、Avalonがどのように適合するかについて非常に役立つアドバイスを提供してくれました。Dave W Smithは、私の最初のインターフェースインジェクション構成コードについて質問し続け、それが愚かであるという事実を突きつけました。Gerry Lowryは、たくさんの誤植の修正を送ってくれました。感謝のしきい値を超えるほどです。

主な改訂

2004年1月23日: インターフェースインジェクションの例の構成コードを書き直しました。

2004年1月16日: Avalonを使用したロケーターとインジェクションの簡単な例を追加しました。

2004年1月14日: 初版