今年の初め、ノースカロライナ州立大学の大学院生達にJ2EEテクノロジーについて講義をする機会がありました。サーブレットとJSPテクノロジーの使い方を説明するデザイン例を紹介し、アプリケーションが永続データを照会、更新する際に、MVCアプローチを簡単なデータ・アクセス・オブジェクト(DAO) にどのように組み合わせることができるかを説明しました。用意した資料を説明していくにつれ、心得顔の笑みとうなずきが学生達の中に見受けられました。しかしながら、講義を終えた後日、1人の学生から次のような電子メールを受け取りました。「先生は何故DAOを1つでなく2つも実装されたのですか? また、その2つを使い分けるためファクトリークラスを用意されたのは何故ですか?」
このメール読んで一瞬戸惑いましたが、しかしすぐに何が問題となっているかに気が付きました。実は、MVCとDAOに加えて3番目のパターンを実装していたのですが、このことを講義できちんと話してはいなかったのです。説明したデザインには、テストを容易にするために、模擬オブジェクト(mock object) を特に使用していたのです。模擬オブジェクトは、DAOを用いて何かを行う際に必須のものなので、どちらか一方だけを実装するなど思いも寄らぬことでした。もう少し補足させていただくならば、テストの容易性を考慮して無意識の内にデザインを調整していたという面がありました。この発想は学生達には馴染みのないものでしたが、経験豊富な開発者にとっても同じことが言えます。
この記事では、擬似データ・アクセス・オブジェクト(SDAO) を用いた層別テストについて説明させていただきます。模擬オブジェクトと同様、SDAOという手法は、既に存在するオブジェクトをテスト用に特に作成されたオブジェクトと置き換えるというものです。しかしながら、SDAOは疑似オブジェクトとは異なり、テスト・アサーションを組み込むために装備する必要があるというものではありません。SDAOパターンが目指しているのは、ある層(ドメイン・オブジェクト層) を他の層 (データ・アクセス層)と分離させることです。では、データ・アクセス・オブジェクトのパターンを見直すことから始めましょう。
データ・アクセス・オブジェクトのパターンの目的は、特殊なデータ・ソースに対して、単一の接点を提供することです。多くのデザイン・パターンと同様、DAOは、各設計作業が独立して進められることを目指しています。特にDAOは、ビジネス・ロジックをデータベース永続性に関するコードと切り離します。データ・アクセス・オブジェクトは、(リレーショナル・データベースに格納されているデータを保持する) オブジェクトの操作を担当します。DAOが操作するオブジェクトは通常値オブジェクト (value objects)と呼ばれますが、しかし、データ転送オブジェクト (DTO) という用語の方が内容をよく表しています。
DAOクラスは、通常CRUDメソッドを含んでいます。すなわち、create()、read()、update()そしてdelete()です。これらは、DTOに対して作用します。例えば今仮に、会議参加者登録システムを開発するとします。DAOクラスのインターフェースは、リスト1で示しているようなものになるでしょう。
リスト1. DAOクラス・インターフェースの例
public interface AttendeeDAO {
public void createAttendee(Attendee person) throws DAOException;
public void updateAttendee(Attendee person) throws DAOException;
public Collection getAllAttendees() throws DAOException;
public void deleteAttendee(Attendee person) throws DAOException;
public Attendee findAttendeeForPrimaryKey(int primaryKey) throws DAOException;
|
このようなインターフェースを宣言することは、DAOパターンを実装する標準的な作業の1つです。DAOの具体的なアプリケーションは、図1に示したインターフェースを実装することになるでしょう。
図1. DAOアプリケーションの例
上に示したようなDAOアプリケーションの多くは、データベース・アクセスにJDBCを使用します。例えばgetAllAttendees()メソッドの場合、クラスは次のような振舞いを示すことでしょう。すなわち、データベースへの接続を取得し、Attendee テーブルの行に関してデータベース照会し、照会操作から戻されたResultSetを繰り返し処理し、ResultSetの各行をもとにAttendee オブジェクトを組み立てます
DAOパターンの更に詳しい解説と例は、参考文献をご覧ください。
擬似データ・アクセス・オブジェクト (SDAO) の本質は、バックエンドに配置されたデータ記憶をシミュレートするということです。SDAOパターンを実装することにより、データベースを実際に配置せずに、各種のアプリケーション層(ビジネス・ロジックやGUIなど) をテストできるようになります。
SDAOを用いて層を分けてテストすることの具体的な利点は、以下のとおりです。
- 安価です: テストとデバッグにデータベースをシミュレートするものを採用することで、開発者の全員のデスクトップPCにDB2などのデータベース・システムを配置する費用を省くことができます。
- 容易です: サーブレット、JSP、EJBという技術を用いてアプリケーションを開発することは、非常に複雑な作業なので、データベース関連のエラー対応も付き物です。層を分けてテストすることで、バックエンドのデータベースを気にかけずに、プレゼンテーション・ロジックとビジネス・ロジックを作り上げることができます。
-
時間がかかりません: 層に分けることで、発生した不具合切り分けることができます。この結果、デバッグの周期が短くなります。エラーの種類によっては (
TransactionRollbackExceptionなどのように) 原因を突き止めるのが難しいものがあります。このような場合、解決すべき問題からデータベース層を取り除くことで、本当の問題に素早く辿り着くことが可能になります。 - 柔軟です: SDAOはパフォーマンスのプロファイル作成とテストに役立ちます。パフォーマンス上の問題の中には、例えばデータベース・デッドロックなどのように、解決のために実データベースを必要とするものもありますが、ドメインとGUIだけに関するパフォーマンス測定値はSDAOを用いて入手できます。これらの層の不具合を解決するために、この情報を利用することができます。
SDAOがどのように機能するかを理解する最良の方法は、それを実際に動かしてみることです。できれば読者の方も試されることをお奨めします。では、リスト2の擬似データ・アクセス・オブジェクトの簡単な例から始めるとしましょう。
リスト2. DefaultDAO
public class DefaultDAO implements AttendeeDAO {
private static DefaultDAO instance = new DefaultDAO();
private Vector attendees = new Vector();
/**
* @see AttendeeDAO#createAttendee(Attendee)
*/
public void createAttendee(Attendee person) throws DAOException {
getAllAttendees().add(person);
}
/**
* @see AttendeeDAO#updateAttendee(Attendee)
*/
public void updateAttendee(Attendee person) throws DAOException {
Attendee match = findAttendeeForPrimaryKey(person.getAttendeeKey());
attendees.remove(match);
attendees.add(person);
}
/**
* @see AttendeeDAO#getAllAttendees()
*/
public Collection getAllAttendees() throws DAOException {
return getAttendees();
}
/**
* @see AttendeeDAO#deleteAttendee(Attendee)
*/
public void deleteAttendee(Attendee person) throws DAOException {
Attendee match = findAttendeeForPrimaryKey(person.getAttendeeKey());
attendees.remove(match);
attendees.add(person);
}
/**
* Gets the attendees
* @return Returns a Vector
*/
public Vector getAttendees() {
return attendees;
}
/**
* Sets the attendees
* @param attendees The attendees to set
*/
public void setAttendees(Vector attendees) {
this.attendees = attendees;
}
/**
* Gets the instance
* @return Returns a DefaultDAO
*/
public static DefaultDAO getInstance() {
return instance;
}
/**
* Sets the instance
* @param instance The instance to set
*/
public static void setInstance(DefaultDAO anInstance) {
instance = anInstance;
}
public Attendee findAttendeeForPrimaryKey(int primaryKey) throws DAOException {
Enumeration enum = attendees.elements();
while (enum.hasMoreElements()) {
Attendee current = (Attendee) enum.nextElement();
if (current.getAttendeeKey() == primaryKey)
return current;
}
throw new DAOException("Primary Key not found " + primaryKey);
}
}
|
ちっとも難しいことはないでしょう?DefaultDAOクラスは、自分のインスタンスを静的変数instanceの中にシングルトンとしてストアします。このことで、getInstance() メソッドでこのインスタンスをアクセスできるようになります。その後、このクラスを使用することで、シングルトンであるインスタンスが保持しているコレクションに対し、Attendee要素を追加、除去、置換できるようになります。
実際の場面でSDAOを活用するには、「本物」のDAOクラスを、新たに作成した「シミュレート版」DAOクラスと、プログラム中で置き換え可能である必要があります。クライアント・コード自身では、Db2AttendeeDAOクラスもDefaultDAOクラスも参照することはできません。そこで、Factoryクラス (別名オブジェクト・ファクトリー) を使用して、クライアント・コードに対し必要に応じDb2AttendeeDAOとDefaultDAOのインスタンスを提供します。
用意したオブジェクト・ファクトリーは、非常に簡単なものです。このオブジェクト・ファクトリーが戻すクラス・インスタンスは2つだけで、(getAttendeeDAO() メソッドで示されている) ソフトウェア的な「スイッチ」を用いてクラス・インスタンスを切り替えています。このスイッチによりシステム属性の値を確認することができます。すなわち、リスト3に示すようにグローバルな値を調べることができます。
リスト3. AttendeeDAOFactory
public class AttendeeDAOFactory {
public static AttendeeDAO getAttendeeDAO() {
String mode = (String) System.getProperty("TestMode");
if (mode.equals("Simulated"))
return DefaultDAO.getInstance();
else
return new DbAttendeeDAO();
}
}
|
テストを行うとき、最初はFactoryスイッチをシミュレート・クラスを戻すようにして始めることと思います。このようにすることで、データベースとは切り離して、システムの他の層をテストできます。テストが後半に至ったところで、「本物」のDAOを戻すようにスイッチを設定することでしょう。図2にFactoryクラスを含んだ最終形の構想を示します。
図2. SDAO実装例
SDAO実装の基本的なことを理解された読者は、シミュレートDAO (DefaultDAO) クラスの違った使い方に挑戦ください。これまでの説明で、最も簡単な実装については、ご理解されたことでしょう。このケースでは、テストをしている間は保持されていなければならないメモリー上のコレクションから結果が取り出されます。このコレクションを、クラスのコンストラクター中のデフォルト値で事前充てんするというのが、先のアイデアの自然な拡張です。先の例で示したシングルトンを使う方法の大きな欠点は、新たなテストの前にシングルトンをクリアしなければならないということです。もしクリアすることを怠ると、後続のテストは失敗してしまいます。幸いなことに、(JUnitなどの) 単体テスト・フレームワークでは、このようなタイプのテストを容易に行うための機能を提供しています。例えば、JUnitにおいては、テストするクラスのteardown()メソッドの中に、シングルトンをクリアするコードを挿入することができ、テストするクラスのsetUp()メソッドの中に事前充てんするコードを挿入できます。
第2番目の方法は、(幾分複雑にはなりますが、より実際のテストには役立つのですが) ファイルから1組のオブジェクトを読み出すのにJava直列化またはXMLを使うというものです。これらのテクノロジーを使うことにより、同一のテストに対し異なる初期条件を設定するために、複数のファイルを利用することができます。
SDAOを用いたテストに対して、多くの場合2通りの方法を用意します。1つ目のDAOは、デフォルトともいえるもので、DTOのメモリー上のコレクションをアクセスします。私はこれをサーブレットやJSPファイルというアプリケーションの上位層を構築しているチームに渡します。そして、2番目のチームのためにデータベースを実際にアクセスするDAOを用意します。このアプローチによって双方のチームは同時に作業を進めることができ、相互のやり取りはDAOインターフェースという共通の規約で規定されます。
IBMのWebSphereを担当しているソフトウェア・サービス・グループでは、多くのお客様とのビジネスに、先に説明した層に分けてテストをする技術を、成功裏に活用してきました。成果物全体の質を向上させる努力に加え、SDAOは我々のチームが各種のJ2EE APIの特性を習熟するのに関して、決定的な役割を果たしました。シミュレートDAOと 実際のDAOを併用することで、アプリケーションの多くの層を同時に開発することができました。この方法により、すべてを1時期に1つにまとめることで発生する複雑さに圧倒されるという事態を回避できました。
最後に、この記事に数々の有用な助言をくださったStacy Joines氏とKen Hygh氏に感謝の意を表します。
- デザイン・パターンに関心をお持ちですか? このアイディアは、通称Gang of Fourと呼ばれる人々が書いたDesign Patterns: Elements of Reusable Object Oriented Software (Erich Gamma, Richard Helms, Ralph Johnson, John Vlissides; Addison-Wesley, 1995)で世に出ました。
- このDesign Patternsに加え、Core J2EE Patterns: Best Practices and Design Strategies (Deepak Alur, John Crupi, Dan Malks; Addison-Wesley, 2001J2EE) も開発者にとって必須といえるものです。
- Martin FowlerによるPatterns of Enterprise Application Architecture (Addison-Wesley, 2002) も企業向け開発に適用するパターンについて、広範囲をカバーする入門書です。
- Paul Mondayによるデザイン・パターンの熱狂的なファンを対象とした高度なチュートリアル"Java design patterns 201" (developerWorks, April 2002)もご覧ください。
- Bruce WhitenackとKyle Brownは共同でリレーショナル・データベースとSmalltalk向けのパターン言語「Crossing Chasms」を開発しました。彼のhomepageで、その開発をKyleのその他のプロジェクト共々ご覧になれます。
- この記事で若干触れた企業向けプログラミング・テクノロジーについては、Kyle Gabhartによる 『J2EEパスファインダー』 シリーズ(developerWorks)をご覧ください。
- ステートレス・ネットワークのためのJ2EEテクノロジー:サーブレット? ステートレス・セッションBean? それとも両方?
- ステートレス・ネットワークのためのJ2EEテクノロジー:適切な解決策を選択するためのベスト・プラクティス
- Sourceforgeのサイトでは、現在Kent BeckとErich GammaによるJUnitを用いた単体テストの入門記事"Test Infected: Programmers Love Writing Tests" (Java Report, July 1998)を掲載しています。
- Alexander Day ChaffeeとWilliam Pietriによる「疑似オブジェクトによる単体テスト」(developerWorks、2002年11月) では、疑似オブジェクトを用いた単体テストを機能拡張するために、どのようにファクトリー・パターンを使用すればよいかについて説明しています。
- Malcolm Davisによる「AntとJUnitを用いた漸進的開発」(developerWorks、2000年11月) では、新しいテクノロジーを紹介すると同時に、単体テストに対する別の観点を提供しています。
- 疑似オブジェクトは、多様な技術とテクノロジーと組み合わせることができます。Nicholas Lesieckiによる「AspectJおよび疑似オブジェクトによる柔軟なテスト」(developerWorks、2002年5月) では、疑似オブジェクトとアスペクト指向プログラミングを連動させる方法について説明しています。
-
developerWorksのJava technologyゾーンでは、Javaプログラミングのすべての側面について数多くの記事を掲載しております。是非ご覧ください。
Kyle BrownはIBM WebSphere Servicesの上級テクニカル・スタッフ・メンバーです。Fortune 500のクライアントに対して、オブジェクト指向プログラミングやJava 2 Enterprise Edition (J2EE) テクノロジーに関するコンサルティングおよび指導を行っています。『Enterprise Java Programming with IBM WebSphere』、『WebSphere 4.0 AEs Workbook for Enterprise JavaBeans (3rd Edition)』、および『The Design Patterns Smalltalk Companion』の著者の1人です。Enterprise Java、OOデザイン、およびデザイン・パターンに関する講演の経験があります。連絡先は、brownkyl@us.ibm.com です。