CodeZine(コードジン)

特集ページ一覧

Inversion of Controlパターンでコンポーネント間の結びつきを弱める

再利用とテストを容易にする疎結合なソフトウェア設計

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/04/14 18:00

ダウンロード ソースコード (10.1 KB)

Inversion of Control(IoC)パターンを使用すると、ソフトウェアコンポーネント間の結び付きが弱まり、再利用とテストが容易になります。このパターンをソフトウェア設計に取り入れる方法を見ていきましょう。

はじめに

 Inversion of Control(IoC:制御の反転)パターンはDependency Injectionパターンとも呼ばれ、最近のJ2EEコミュニティではよく利用されています。Spring PicoContainerHiveMindのように、IoCパターンを使用して軽量J2EEコンテナを開発しているオープンソースプロジェクトもいくつかあります。

 しかし、IoCは新しい概念ではありません。このパターンは数年前から利用されています。IoCパターンでは、インターフェイス、継承、ポリモーフィズムといったオブジェクト指向設計の原則および特徴を使用して、ソフトウェアコンポーネントの結び付きを弱め、コンポーネントの再利用とテストが容易になるようなソフトウェア設計を実現します。

 本稿では、IoCパターンの概要を説明し、オープンソースのIoCフレームワークをまったく実装せずにIoCパターンをソフトウェア設計に取り入れる方法を紹介します。

IoCデザインパターン

 クラスAとクラスBの間に、クラスAがクラスBのサービスを使用するという関係があるとします。この関係を実現する一般的な方法は、クラスAの内部でクラスBをインスタンス化することです。この方法でも動作上は問題ありませんが、クラス間の結び付きが強くなってしまいます。クラスAを修正せずにクラスBを簡単に変更することができないからです。

 クラス間の結び付きを弱めるには、クラスBのインスタンス(オブジェクト"b")をクラスAのインスタンス(オブジェクト"a")に注入するConfiguratorを用意します。後からクラスBの実装を変更したくなった場合は、Configuratorオブジェクトを変更するだけで済みます。

 つまり、オブジェクト"a"がオブジェクト"b"の参照を取得する方法の制御が反転しています。オブジェクト"a"は、オブジェクト"b"の参照を取得する責任を持ちません。その代わりに、Configuratorがこれを担当します。これがIoCデザインパターンの基本です。

Configuratorオブジェクトのメリット

 このConfiguratorオブジェクトのメリットを具体的に示すために、以降では、IoCを使用しない設計と、IoCを使用した設計の簡単な例を紹介します。

 リスト1と図1は、クラスAがクラスBを使用する設計の簡単な例です。

図1 オブジェクト"a"がオブジェクト"b"を直接作成する
図1 オブジェクト"a"がオブジェクト"b"を直接作成する
リスト1 クラスAがクラスBを直接参照する
public class A{
  private B b;

  public A(){
    b=new B();
}

 リスト1は、次のような設計上の条件を前提としています。

  1. クラスAはクラスBへの参照を必要とする
  2. クラスBはデフォルトコンストラクタを持つ具象クラスである
  3. クラスAはクラスBのインスタンスを所有する
  4. 他のクラスはクラスBのインスタンスにアクセスできない

 上記の条件がどれか1つでも変更された場合は、リスト1のコードを修正しなければなりません。たとえば、クラスBの設計を変更して、デフォルトコンストラクタを使用する代わりにクラスC(図2を参照)を受け取るようにした場合は、リスト1をリスト2のように変更することになります。

図2 オブジェクト"a"がまずオブジェクト"c"を作成し、オブジェクト"c"を渡すことでオブジェクト"b"を作成する
図2 オブジェクト"a"がまずオブジェクト"c"を作成し、オブジェクト"c"を渡すことでオブジェクト"b"を作成する
リスト2 クラスAがクラスBとクラスCを直接参照する
public class A{
  private B b;

  public A(){
   C c=new C();
    b=new B(c);
}

 このリスト2も、いくつかの設計上の条件を前提にしています。今度はオブジェクト"a"がオブジェクト"b"とオブジェクト"c"の両方を所有しています。クラスBまたはクラスCを大幅に変更した場合は、クラスAも修正の必要があります。つまり、暗黙的な前提に基づいた単純なクラスの単純な設計では、将来的な保守に大きな負担がかかるおそれがあります。

 ここで紹介したのはごく単純な例ですが、さまざまなクラスを使用する一般的なアプリケーションであれば、変更がどれだけ難しいか容易に想像できるでしょう。

 それに対して、IoCパターンを使用する設計の場合は、オブジェクト"b"を作成する責任をオブジェクト"a"からIoCフレームワークに移し、そのフレームワークでオブジェクト"b"を作成してオブジェクト"a"に注入するようにします。これにより、クラスAをクラスBの修正から切り離すことができます。オブジェクト"a"はオブジェクト"b"への参照を必要としますが、この点は、IoCフレームワークがオブジェクト"b"をオブジェクト"a"に注入することで解決されます。

 リスト3は、前述のリストのクラスAをIoCパターンを使用するように修正したものです。

図3 IoCフレームワークがオブジェクト"b"を作成し、それをオブジェクト"a"に注入する
図3 IoCフレームワークがオブジェクト"b"を作成し、それをオブジェクト"a"に注入する
リスト3 クラスAはsetBを通じてクラスBへの参照を取得する
public class A{
  private B b;

  public A(){
  }
 
  public setB(B b){
   this.b=b;
 } 

}

 リスト3は、次のような設計上の条件を前提としています。

  1. AはBを参照する必要があるが、Bをインスタンス化する方法は知らなくてよい
  2. Bはインターフェイスでも抽象クラスでも具象クラスでもよい
  3. クラスAのインスタンスを使用する前に、クラスBのインスタンスへの参照を用意する必要がある

 この設計上の条件を見ると、クラスAとクラスBの結び付きが弱くなっていることがわかります。どちらのクラスも、相手に影響を与えることなく個別に修正できます。もちろん、クラスBのパブリックメソッドに変更があった場合は、クラスAも変更する必要があります。しかし、オブジェクト"b"の作成方法と管理方法は、オブジェクト"a"の実装内では定義されていません。その代わりに、IoCフレームワークがオブジェクト"a"内のsetB()メソッドを使用してオブジェクト"b"を注入します(図3を参照)。

IoCフレームワークの種類

 いくつかのオープンソースIoCフレームワーク(SpringPicoContainerHiveMind など)は、IoCパターンをサポートしています。IoCの基本原則は単純ですが、これらのフレームワークはそれぞれ異なる実装をサポートしており、異なるメリットを実現しています。

 IoCパターンには3種類の実装方法があり、それぞれ「Setterベース」、「コンストラクタベース」、「インターフェイスベース」と呼ばれています。ここではそれぞれの方法について簡単に説明します。詳細については、各フレームワークのホームページを参照してください。

SetterベースIoC

 このタイプのIoCでは、Setterメソッドを使用して、参照される側のオブジェクトを参照する側のオブジェクトに注入します。これは最も一般的なIoCであり、SpringとPicoContainerはこの方式を実装しています。「SetterベースIoC」は、オプションのパラメータを受け取るオブジェクトや、ライフサイクル中にプロパティを何度も変更する必要があるオブジェクトに適しています。

 この方式の主な短所は、Setterメソッドを使用するときにオブジェクトのすべてのプロパティを外部に公開しなければならないことです。これは、オブジェクト指向の基本原則である「データのカプセル化」と「情報の隠蔽」に反します。

コンストラクタベースIoC

 このタイプのIoCでは、コンストラクタを使用してオブジェクトの参照を設定します。この方式の主な長所は、参照されるオブジェクトを知っているのが作成者だけであるという点です。いったんオブジェクトが作成されると、そのオブジェクトを使用するクライアントコードは、参照されるオブジェクトを認識しません。

 この方式は、すべてのアプリケーションで使用できるわけではありません。たとえば、デフォルトコンストラクタを必要とする外部APIを使用する場合は、「SetterベースIoC」を採用しなければなりません。Springの実装では主に「コンストラクタベースIoC」が使用されています。

インターフェイスベースIoC

 このタイプのIoCでは、IoCフレームワークの特殊なインターフェイスをオブジェクトに実装することで、IoCフレームワークがオブジェクトを適切に注入できるようにします。この方式の主な長所は、オブジェクト参照を設定するための外部設定ファイルが必要ないことです。その代わりに、IoCフレームワークのマーカーインターフェイスを実装することで、オブジェクトをどのように結合するかをIoCフレームワークに認識させます。これはEJBを使用するのに似ています。EJBコンテナは、オブジェクトをインスタンス化して自身にフックさせる方法を認識しています。

 この方式の主な短所は、マーカーインターフェイスを使用するために、特定のIoCフレームワークとの結び付きが強くなってしまうことです。Apache Avalonはこの方式に基づいていますが、このプロジェクトは既に閉鎖されています。

IoCの例

 新しいプロジェクトを開始する場合は、必要に応じていずれかのオープンソースIoCフレームワークを選択することができます。しかし、既存のプロジェクトでIoCパターンを使用する場合は、IoCをサポートする独自のクラスを作成する必要があります。オープンソースのIoCフレームワークは既成のコンポーネントや充実した機能を提供してくれますが、これらを使用せずに、IoCパターンをサポートする一連のクラスを独自に作成することもできます。ここではその方法を紹介します。

 たとえば、顧客データを処理するCustomerServiceオブジェクトを作成したいとします。処理対象の顧客データは、リレーショナルデータベースとXMLファイルの2か所に格納されています。さらに、このCustomerServiceオブジェクト内で、データベースまたはXMLファイルからデータを読み取ってCustomerオブジェクトを作成したいとします。このアプリケーションを作成するに当たり、CustomerServiceのソースコードを変更せずにデータソースをデータベースとXMLファイルの間で切り替えられるようにするには、どのような設計にしたらよいでしょうか。

DataSource

 まずインターフェイスを設計し(リスト4を参照)、顧客データの取得および保存に使用する共通メソッドを定義します。XMLDataSourceクラスとJDBCDataSourceクラスは両方ともこのインターフェイスを実装します。

リスト4 顧客データの読み書きに使用する共通インターフェイス
public interface DataSource {
    public Object retrieveObject();
    public void setDataSourceName(String name);
    public String getDataSourceName();
    public void storeObject(Object object);
}

XMLDataSource

 リスト5のXMLDataSourceクラスは、DataSourceインターフェイスを実装し、XMLファイルに対して顧客データの読み書きを行う実装を作成します。

リスト5 XMLファイルから顧客データを取得するクラス
public class XMLDataSource implements DataSource {
    private String name;
    
/**
* Default Constructor
*/
public XMLDataSource() { super(); } /**
* Retrieve Customer data from XML Source and construct
* Customer object
*/
public Object retrieveObject() { //get XML data, parse it and then construct //Customer object return new Customer("XML",10); } /**
* Set the DataSource name
*/
public void setDataSourceName(String name) { this.name=name; } /**
* Return DataSource name
*/
public String getDataSourceName() { return name; } /**
* Store Customer into XML file
*/
public void storeObject(Object object) { //Retrieve customer data and store it in //XML file } }

RelationalDataSource

 リスト6のRelationalDataSourceクラスも、DataSourceインターフェイスを実装し、XMLDataSourceと同じ実装を作成しますが、顧客データをリレーショナルデータベースとの間で読み書きするという点だけが異なります。

リスト6 リレーショナルデータベースから顧客データを取得するクラス
public class RelationalDataSource implements DataSource {
    private String name;
    
    /**
* Default constructor
*/
public RelationalDataSource() { super(); } /**
* Using the DataSource retrieve data for Customer and build a
* Customer object to return it to the caller
*/
public Object retrieveObject() { //get data for Customer object from DB and create a //Customer object return new Customer("Relational",10); } /**
* Set the DataSource name
*/
public void setDataSourceName(String name) { this.name=name; } /**
* Return the name of the DataSource
*/
public String getDataSourceName() { return name; } /**
* Store Customer into relational DB
*/
public void storeObject(Object object) { //store the customer data into Relational DB } }

Customer

 リスト7のCustomerクラスは、顧客データを格納する単純なクラスです。このクラスはXMLDataSourceまたはRelationalDatSourceオブジェクトによって作成されます。

リスト7 顧客データを格納するクラス
public class Customer {
    private String name;
    private int age;
    
    /**
* Default Constructor
*/
public Customer(String name, int age) { this.name=name; this.age=age; } /**
* @return Returns the age.
*/
public int getAge() { return age; } /**
* @param age The age to set.
*/
public void setAge(int age) { this.age = age; } /**
* @return Returns the name.
*/
public String getName() { return name; } /**
* @param name The name to set.
*/
public void setName(String name) { this.name = name; } }

CustomerService

 リスト8のCustomerServiceクラスは、DataSourceへの参照を受け取り、これを使用してCustomerオブジェクトの取得と保存を行います。DataSourceの実際の実装は、XMLDataSourceまたはRelationalDataSourceになります。どちらになるかは、どちらのオブジェクトがCustomerServiceオブジェクトに注入されるかによって決まります。CustomerServiceオブジェクトのコンストラクタはDataSourceオブジェクトの具象実装を受け取り、それを使用して顧客データの読み書きを行います。

リスト8 ServiceConfiguratorを通じてDataSourceの参照を取得するCustomerServiceクラス
public class CustomerService {
    private DataSource dataSource;
    private Customer customer;
    
    /**
* Constructor in which DataSource object is injected. Based on the
* ioc.properties this object can either refer to RelationlDataSource or
* XMLDataSource
*/
public CustomerService(DataSource dataSource) { super(); this.dataSource=dataSource; customer=(Customer)dataSource.retrieveObject(); } /**
* Modify Customer name
* @param name
*/
public void updateCustomerName(String name) { customer.setName(name); } /**
* Modify Customer age
* @param age
*/
public void updateCustomerAge(int age){ customer.setAge(age); } /**
*
* @return Customer name
*/
public String getCustomerName(){ return customer.getName(); } /**
*
* @return Customer age
*/
public int getCustomerAge(){ return customer.getAge(); } }

ServiceConfigurator

 リスト9のServiceConfiguratorは、IoCパターンを使用してDataSourceの具象実装をCustomerServiceオブジェクトに注入するメインクラスです。このクラスは「ioc.properties」ファイルから設定パラメータを読み取り(図4を参照)、XMLDataSourceオブジェクトとRelationalDataSourceオブジェクトのどちらを作成するかを判断します。

 DataSourceオブジェクトはデフォルトコンストラクタを使用して作成され、その名前はSetterメソッドを使用して設定されます。作成されたDataSourceオブジェクトは、CustomerServiceのコンストラクタ(DataSourceの参照を受け取ります)によってCustomerServiceに注入されます。

 作成されたCustomerServiceオブジェクトはServiceConfiguratorのレジストリに保存されます。これ以降にCustomerServiceオブジェクトが要求されたときは、このオブジェクトが返されます。

 図4にServiceConfiguratorの内部的な処理を示します。図5は、ここまでに説明したすべてのクラスに関するUMLダイアグラムです。「ioc.properties」ファイルを編集するだけで、XMLDataSourceRelationalDataSourceを切り替えることができます。

図4 ServiceConfiguratorを使用するIoC
図4 ServiceConfiguratorを使用するIoC
図5 ServiceConfiguratorのUMLダイアグラム
図5 ServiceConfiguratorのUMLダイアグラム
リスト9 IoCを使用してDataSourceをCustomerServiceに注入するServiceConfigurator

public class ServiceConfigurator {
    public static final String IoC_CONFIG_FILE="ioc.properties";
    private Properties props;
    private HashMap serviceRegistery;
    private static ServiceConfigurator thisObject;
    
    /**
* This method first checks if there is a ServiceConfigurator instance
* exist, if not creates a one, stored it and returns it
* @return
*/
public static ServiceConfigurator createServiceConfigurator(){ if(thisObject==null){ thisObject=new ServiceConfigurator(); } return thisObject; } /**
* Private Constructor makes this class singleton
*/
private ServiceConfigurator() { props = new Properties(); serviceRegistery=new HashMap(); loadIoCConfig(); createServices(); } /**
* Load the IoC_CONFIG_FILE properties file
*
*/
public void loadIoCConfig(){ InputStream is = this.getClass().getClassLoader().getResourceAsStream(IoC_CONFIG_FILE); try { props.load(is); is.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } /**
* Create the CustomerService by getting the DataSource name from the
* properties file. The CustomerService object is stored in the
* serviceRegistery so that it will be retrieved when requested.
* During the construction of CustomerService the DataSource object
* is injected into it. So the CustomerService can access the DataSource
* to retrieve the Customer.
*
*/
public void createServices(){ String dataSourceName=props.getProperty("dataSource"); DataSource dataSource=null; if(dataSourceName!=null){ try { dataSource=(DataSource)Class.forName(dataSourceName).newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } //name of the DataSource is retrieved from the properties file dataSource.setDataSourceName(props.getProperty("name")); CustomerService customerService=new CustomerService(dataSource); serviceRegistery.put(CustomerService.class,customerService); } /**
* Stored Service is retrieved from serviceRegistery for the given
* Class object
*
* @param classObj
* @return
*/
public Object getService(Class classObj){ return serviceRegistery.get(classObj); } }

 このServiceConfiguratorは、IoCパターンをサポートする単純なクラスです。これを修正して、複数のDataSourceオブジェクトを同時にサポートする機能や、DataSourceCustomerServiceに動的に注入する機能、ライフサイクル管理機能などを追加することができます。

CustomerServiceTester

 リスト10のCustomerServiceTesterクラスは、CustomerServiceを使用するJUnitテストです。このテストでは、ServiceConfiguratorを使用してCustomerServiceオブジェクトを取得し、このオブジェクトを使って顧客の名前と年齢を変更します。

リスト10 CustomerServiceを使用するJUnitテスト
public class CustomerServiceTester extends TestCase{
    private ServiceConfigurator serviceConfig;
    
    /**
*Default Constructor
*/
public CustomerServiceTester() { super(); } /**
* Create ServiceConfigurator
*/
public void setUp() throws Exception{ super.setUp(); serviceConfig=ServiceConfigurator.createServiceConfigurator(); } /**
* Test CustomerService and check for Customer
* @throws Exception
*/
public void testCustomerService() throws Exception{ CustomerService custService = (CustomerService)serviceConfig.getService(CustomerService.class); assertNotNull(custService); custService.updateCustomerAge(30); custService.updateCustomerName("Mani"); assertEquals(30,custService.getCustomerAge()); assertEquals("Mani",custService.getCustomerName()); }

Service ConfiguratorとService Locatorの違い

 リスト9のServiceConfiguratorクラスの実装を見て、IoCパターンを使用するServiceConfiguratorクラスと、Service Locatorパターンを使用するServiceConfiguratorクラスとではどこが違うのかと思った人もいるのではないでしょうか。

 実際、リスト8のCustomerServiceクラスを、Service Locatorを使用するように修正することもできます。その場合は、Service Locatorの内部実装に従って、CustomerServiceクラスにXMLDataSourceオブジェクトとRelationalDataSourceオブジェクトのどちらかを渡すことになります。

 Service Locatorの内部実装を変更すれば、CustomerServiceCustomerを取得するときに使用するDataSourceを切り替えることができます。このコンテキストでは、Service LocatorとService ConfiguratorはCustomerServiceに対して同じ機能を提供します。

 両者の主な違いは、CustomerServiceDataSource参照を取得する方法です。Service Locatorの場合は、CustomerServiceがService LocatorからDataSource参照を直接取得する必要があります。

 一方、Service Configuratorの場合は、CustomerServiceServiceConfiguratorクラスを通じて間接的にDataSourceを取得します。

 では、Service Locatorに比べてService Configuratorを使用した場合のメリットは何でしょうか。Service Configuratorを使用した場合は、DataSourceの取得やインスタンス化といった処理をCustomerServiceが直接担当しなくて済みます。そのため、CustomerServiceを簡単にテストすることができます。適切なDataSourceを注入するだけでよいからです。

 さらに、リスト1とリスト2のところで説明したとおり、CustomerServiceDataSource参照を取得する方法についての前提条件がないので、さまざまなアプリケーションでCustomerServiceオブジェクトを再利用することができます。これが、Service LocatorではなくService Configuratorを使用することの主なメリットです。

何を注入するか

 図5に示したとおり、CustomerServiceDataSourceクラスとCustomerクラスの両方と関係を持っています。本稿では、DataSourceCustomerServiceに注入する方法を説明してきましたが、なぜCustomerCustomerServiceに注入しなかったのでしょうか。

 実際には、Customer用のインターフェイスを作成し、それをCustomerService内で参照するようにして、ServiceConfiguratorCustomerオブジェクトをCustomerServiceに注入するという設計にしてもまったく問題ありません。そうするかどうかは、アプリケーションの要件と、CustomerServiceクラスの設計方法しだいです。

 今回の例では、Customerオブジェクトの作成と保存をDataSourceに担当させているため、DataSourceCustomerが密接に結び付いています。将来的にCustomerクラスを変更するときには、DataSourceのすべての実装を修正しなければなりません。しかし、この設計は、Customerのパブリックメソッドは一切変更されず、Customerの内部で変更が行われてもDataSourceには影響が及ばないということを前提として採用されたものです。

 このアプリケーションで予想されるのは、顧客データの取得/保存方法の変更だけです。現時点では、顧客データはリレーショナルデータベースかXMLファイルに格納されています。将来的には、オブジェクトデータベースに保存したり、Webサービスを通じて取得したりする可能性もありますが、どちらのシナリオでも、顧客データの取得と保存を行う新しいクラスを作成すれば対処できます。したがって、上記の前提に基づき、今回のサンプルではDataSourceだけをCustomerServiceに注入し、このオブジェクトを使用して顧客データの取得と保存を行っています。

終わりに

 本稿では、IoCパターンの概要を説明し、このパターンを使用するオープンソースフレームワークを簡単に紹介しました。さらに、ServiceConfiguratorというサンプルを通じて、既存のアプリケーションにIoCパターンを取り入れる方法を説明しました。ServiceConfiguratorは単純なクラスですが、IoCパターンをサポートし、このパターンの長所を十分に表しています。必要に応じてこのServiceConfiguratorを修正し、さまざまな機能を追加してみてください。



  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

あなたにオススメ

著者プロフィール

  • japan.internet.com(ジャパンインターネットコム)

    japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.com や EarthWeb.c...

  • Mani Malarvannan(Mani Malarvannan)

    Cybelink Systemsのコンサルタント。ここ数年はオブジェクト指向プログラミングとソフトウェアパターンに基づくWebベースアプリケーション開発に従事。

バックナンバー

連載:japan.internet.com翻訳記事

もっと読む

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5