はじめに
Inversion of Control(IoC:制御の反転)パターンはDependency Injectionパターンとも呼ばれ、最近のJ2EEコミュニティではよく利用されています。Spring 、PicoContainer、HiveMindのように、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"を直接作成する](http://cz-cdn.shoeisha.jp/static/images/article/60/12520.gif)
public class A{ private B b; public A(){ b=new B(); }
リスト1は、次のような設計上の条件を前提としています。
- クラスAはクラスBへの参照を必要とする
- クラスBはデフォルトコンストラクタを持つ具象クラスである
- クラスAはクラスBのインスタンスを所有する
- 他のクラスはクラスBのインスタンスにアクセスできない
上記の条件がどれか1つでも変更された場合は、リスト1のコードを修正しなければなりません。たとえば、クラスBの設計を変更して、デフォルトコンストラクタを使用する代わりにクラスC(図2を参照)を受け取るようにした場合は、リスト1をリスト2のように変更することになります。
![図2 オブジェクト"a"がまずオブジェクト"c"を作成し、オブジェクト"c"を渡すことでオブジェクト"b"を作成する 図2 オブジェクト"a"がまずオブジェクト"c"を作成し、オブジェクト"c"を渡すことでオブジェクト"b"を作成する](http://cz-cdn.shoeisha.jp/static/images/article/60/12521.gif)
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"に注入する](http://cz-cdn.shoeisha.jp/static/images/article/60/12522.gif)
public class A{ private B b; public A(){ } public setB(B b){ this.b=b; } }
リスト3は、次のような設計上の条件を前提としています。
- AはBを参照する必要があるが、Bをインスタンス化する方法は知らなくてよい
- Bはインターフェイスでも抽象クラスでも具象クラスでもよい
- クラスAのインスタンスを使用する前に、クラスBのインスタンスへの参照を用意する必要がある
この設計上の条件を見ると、クラスAとクラスBの結び付きが弱くなっていることがわかります。どちらのクラスも、相手に影響を与えることなく個別に修正できます。もちろん、クラスBのパブリックメソッドに変更があった場合は、クラスAも変更する必要があります。しかし、オブジェクト"b"の作成方法と管理方法は、オブジェクト"a"の実装内では定義されていません。その代わりに、IoCフレームワークがオブジェクト"a"内のsetB()
メソッドを使用してオブジェクト"b"を注入します(図3を参照)。
IoCフレームワークの種類
いくつかのオープンソースIoCフレームワーク(Spring、PicoContainer、HiveMind など)は、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
クラスは両方ともこのインターフェイスを実装します。
public interface DataSource { public Object retrieveObject(); public void setDataSourceName(String name); public String getDataSourceName(); public void storeObject(Object object); }
XMLDataSource
リスト5のXMLDataSource
クラスは、DataSource
インターフェイスを実装し、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
と同じ実装を作成しますが、顧客データをリレーショナルデータベースとの間で読み書きするという点だけが異なります。
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
オブジェクトによって作成されます。
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
オブジェクトの具象実装を受け取り、それを使用して顧客データの読み書きを行います。
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」ファイルを編集するだけで、XMLDataSource
とRelationalDataSource
を切り替えることができます。
![図4 ServiceConfiguratorを使用するIoC 図4 ServiceConfiguratorを使用するIoC](http://cz-cdn.shoeisha.jp/static/images/article/60/12524.gif)
![図5 ServiceConfiguratorのUMLダイアグラム 図5 ServiceConfiguratorのUMLダイアグラム](http://cz-cdn.shoeisha.jp/static/images/article/60/12525.gif)
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
オブジェクトを同時にサポートする機能や、DataSource
をCustomerService
に動的に注入する機能、ライフサイクル管理機能などを追加することができます。
CustomerServiceTester
リスト10のCustomerServiceTester
クラスは、CustomerService
を使用するJUnitテストです。このテストでは、ServiceConfigurator
を使用してCustomerService
オブジェクトを取得し、このオブジェクトを使って顧客の名前と年齢を変更します。
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の内部実装を変更すれば、CustomerService
がCustomer
を取得するときに使用するDataSource
を切り替えることができます。このコンテキストでは、Service LocatorとService ConfiguratorはCustomerService
に対して同じ機能を提供します。
両者の主な違いは、CustomerService
がDataSource
参照を取得する方法です。Service Locatorの場合は、CustomerService
がService LocatorからDataSource
参照を直接取得する必要があります。
一方、Service Configuratorの場合は、CustomerService
はServiceConfigurator
クラスを通じて間接的にDataSource
を取得します。
では、Service Locatorに比べてService Configuratorを使用した場合のメリットは何でしょうか。Service Configuratorを使用した場合は、DataSource
の取得やインスタンス化といった処理をCustomerService
が直接担当しなくて済みます。そのため、CustomerService
を簡単にテストすることができます。適切なDataSource
を注入するだけでよいからです。
さらに、リスト1とリスト2のところで説明したとおり、CustomerService
がDataSource
参照を取得する方法についての前提条件がないので、さまざまなアプリケーションでCustomerService
オブジェクトを再利用することができます。これが、Service LocatorではなくService Configuratorを使用することの主なメリットです。
何を注入するか
図5に示したとおり、CustomerService
はDataSource
クラスとCustomer
クラスの両方と関係を持っています。本稿では、DataSource
をCustomerService
に注入する方法を説明してきましたが、なぜCustomer
をCustomerService
に注入しなかったのでしょうか。
実際には、Customer
用のインターフェイスを作成し、それをCustomerService
内で参照するようにして、ServiceConfigurator
がCustomer
オブジェクトをCustomerService
に注入するという設計にしてもまったく問題ありません。そうするかどうかは、アプリケーションの要件と、CustomerService
クラスの設計方法しだいです。
今回の例では、Customer
オブジェクトの作成と保存をDataSource
に担当させているため、DataSource
とCustomer
が密接に結び付いています。将来的にCustomer
クラスを変更するときには、DataSource
のすべての実装を修正しなければなりません。しかし、この設計は、Customer
のパブリックメソッドは一切変更されず、Customer
の内部で変更が行われてもDataSource
には影響が及ばないということを前提として採用されたものです。
このアプリケーションで予想されるのは、顧客データの取得/保存方法の変更だけです。現時点では、顧客データはリレーショナルデータベースかXMLファイルに格納されています。将来的には、オブジェクトデータベースに保存したり、Webサービスを通じて取得したりする可能性もありますが、どちらのシナリオでも、顧客データの取得と保存を行う新しいクラスを作成すれば対処できます。したがって、上記の前提に基づき、今回のサンプルではDataSource
だけをCustomerService
に注入し、このオブジェクトを使用して顧客データの取得と保存を行っています。
終わりに
本稿では、IoCパターンの概要を説明し、このパターンを使用するオープンソースフレームワークを簡単に紹介しました。さらに、ServiceConfigurator
というサンプルを通じて、既存のアプリケーションにIoCパターンを取り入れる方法を説明しました。ServiceConfigurator
は単純なクラスですが、IoCパターンをサポートし、このパターンの長所を十分に表しています。必要に応じてこのServiceConfigurator
を修正し、さまざまな機能を追加してみてください。