第3章 逆引きカタログ J2EE編
Authors:Agata Toshitaka
みなさんのサーブレット(Strutsを使用している場合はアクションクラス)の行数は、平均どれくらいでしょうか?
データベースアクセスや業務処理など、すべての処理をサーブレットに詰め込もうとして、あっという間に1000行を越すような「太ったサーブレット」を作ってしまったことありませんか?
サーブレットを初めて書いたときは筆者もそうでした。
このような「長く」「すべての処理が入った」サーブレットのことをすべてのことを行う魔法のようなサーブレットということで「マジックサーブレット」と呼びます。
マジックサーブレットは保守や機能拡張が難しいのはもちろんのこと、「アプリケーションが提供する機能」を把握することが難しくなるという弊害があります。
機能を把握できないと「あの機能ってどこにあったっけ?」という状況を生み出しがちになります。
そのような状況を避けるためにも、「サービスを提供するレイヤー(層)」をはっきりとさせ、アプリケーションの見通しをよくしておくことは大切なことです。
本節では、マジックサーブレットに機能分割のリファクタリングを施していくことで、Webアプリケーションに「サービスを提供するレイヤ」を持たせる方法を解説します。
具体的には「ファサード」というパターンを使います。ファサードは複雑な処理の呼び出しを単純化するためのパターンです。
マジックサーブレットの長くて複雑な業務処理は別のクラスに移動させて、サーブレットからは「業務処理を呼び出すだけ」という構成にしていきます。
今回ご紹介する「サービスを提供するレイヤ」を作るためのファサードパターンは、
J2EEパターンではセッションファサードパターンと呼ばれています。
セッションファサードはEJBのセッションBeanを対象としたパターンです。
本特集ではEJBを対象としないため、セッションファサードという名前は使用せず、単にファサードと呼んでいますが、パターンの目的・手法はほとんど同じです。
ファサードは「建物の正面」という意味です。
筆者はファサードのことを考えるときは大きな建物の「シンプルな正面玄関」をイメージしています。
「シンプルな正面玄関」の奥には複雑な処理が隠されていますが、呼び出す側は「シンプルな正面玄関=シンプルなメソッド」だけを意識すればよいわけです(図5)。
まずは簡単な例を見てみましょう。
図6の左側のdoGet()メソッドは「パラメータの取得」「データベース処理」「業務処理」などが渾然一体となって、かなり複雑でマジックなサーブレットと化しています。
図6の右側のように複雑な業務処理の部分を「ファサード」クラスに移動・分割することで、サーブレットからは「ファサードを呼び出すだけ」の状態にすることができます。
より具体的な例を見てみましょう。
サンプルは、シンプルな書籍データの更新・挿入処理を行うものです。
今回はサーブレットでなくStrutsのアクションクラスを題材にしています。
Strutsのアクションクラスも役割としてはサーブレットとまったく同じですので、複雑な「マジックアクション」は極力避けるべきです。
ここでは、「マジックアクションの状態」から、ファサードにより「アプリケーションの提供する機能が分割された状態」にリファクタリングを施していきます。
| Strutsアクションクラスの役割 |
Strutsのアクションクラスの役割はWebコントローラと呼ばれます。
WebコントローラはWeb特有の処理を行い、業務処理との橋渡しを行うのが目的です。
つまりリスト4-D「次画面への遷移」やリスト5-@「フォームBeanからドメインオブジェクトにデータをコピー」がアクションクラスにあることはまったく問題ありません。
|
◎第1段階:マジックアクション
まずは全ての処理がアクションクラスに実装されたマジックアクションを見てみましょう(
リスト4)。
リスト4 UpdateBookAction1.java
public class UpdateBookAction1 extends Action {
private static Log log = LogFactory.getLog(UpdateBookAction1.class);
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest req, HttpServletResponse res)
throws Exception {
UpdaateBookForm bookForm = (UpdateBookForm) form;
Connection conn = null;
PreparedStatement st = null;
PreparedStatement st2 = null;
try {
Class.forname("org.hsqldb.jdbcDriver");
conn = DriverManager.getConnection(
"sa", "", "jdbc:hsqldb:hsql://localhost");
st = conn.prepareStatement(
"update tdl_book set title=?, price=? where id=?");
st.setString(1, bookForm.getTitle());
st.setInt(2, bookForm.getPrice());
st.setInt(3, bookForm.getId());
int result = st.exexuteUpdate();
if (result == 0) {
st2 = conn.prepareStatement(
"insert into tdl_book(id, title, price) "
+ "values(?, ?, ?)");
st2.setInt(1, bookForm.getId());
st2.setString(2, bookForm.getTitle());
st2.setInt(3, bookForm.getPrice());
int result2 = st.exexuteUpdate();
}
conn.commit();
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new SQLRuntimeException(e);
} finally {
if (st != null) { st.close(); }
if (st2 != null) { st2.close(); }
if (conn != null) { conn.close(); } …C
}
return mapping.findForward("success"); …D
}
}
execute()メソッドでは以下の処理が行われています。
- @コネクションの取得
- A更新SQLの発行
- B更新件数が0だった場合、挿入処理とみなして、挿入のSQLを発行
- Cコネクションを閉じる
- D次画面への遷移
このうち@〜Cまでが更新・挿入処理に当たりますが、アクションクラスに直接処理が埋め込まれているため、他
のクラスからこの機能を呼び出すことはできません。
そこで、データベースにアクセスするクラスは後述のDAO(データアクセスオブジェクト)に切り出してみたいと思います。
◎第2段階:データベースにアクセスするコードを別クラスに分割
データベースにアクセスするコードを別クラスに分割したものが
リスト5・
リスト6です。
リスト5 UpdateBookAction2.java
public class UpdateBookAction2 extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest req, HttpServletResponse res)
throws Exception {
UpdateBookForm bookForm = (UpdateBookForm) form;
Book book = new Book();
BeanUtils.copyProperties(book, form); …A| B |
// 更新処理
BookDao bookDao = new BookDao();
int result = bookDao.update(book);
// 更新件数が0なら挿入処理
if (result == 0) {
// 挿入処理
bookDao.insert(book);
}
bookDao.commit();
|
return mapping.findForward("success");
}
}
リスト6 BookDao.java
public class BookDao {
private static Log log =
LogFactory.getLog(BookDao.class);
Connection conn;
public BookDao() {
conn = SqlUtil.createConnection();
}
public int update(Book book) {
preparedStatement st = null;
try {
st = conn.prepareStatement(
"update tdl_book set title=?, price=? where id=?";
st.setString(1, book.getTitle());
st.setInt(2, book.getPrice());
st.setInt(3, book.getId());
return st.executeUpdate();
} catch {
log.error(e.getMessage(), e);
throw new SQLRuntimeException(e);
} finally {
sqlUtil.close(st);
}
}
public int insert(Book book) {
preparedStatement st = null;
try {
st = conn.prepareStatement(
"insert into tdl_book(id, title, price) "
+ "values(?, ?, ?)");
st.setInt(1, book.getId());
st.setString(2, book.getTitle());
st.setInt(3, book.getPrice());
return st.executeUpdate();
} catch(SQLException e) {
log.error(e.getMessage(), e);
throw new SQLRuntimeException(e);
} finally {
sqlUtil.close(st);
}
}
public void commit() {
sqlUtil.commit(conn);
}
}
コネクションの取得、SQLの発行などをBookDaoクラスに移動しました。
たったこれだけで、アクションクラスはすっきり見やすくなりました。
この変更でBookDaoクラスはUpdateBookAction2以外のアクションクラスから呼び出すことができるようになりました。
その他変更点としては
リスト5-AでJakarta CommonsのJavaBeans操作用コンポーネントであるBeanUtils を使って、UpdateBookFormからBookオブジェクトにデータのコピーをおこなっています。
このことでBookDaoにStruts固有のクラスであるフォームBeanを渡さないようにしています。
「別にUpdateBookFormを直接渡してもいいんじゃないの?」という声が聞こえてきそうですが、UpdateBookFormを直接渡すとBookDaoはUpdateBookFormに極度に依存します。
この業務専用のBookオブジェクトをきちんと作ってを渡すことで、BookDaoはさまざまな場所で利用できる「汎用性の高い」クラスになります。
またコネクションの取得、クローズなどの共通的な処理はSqlUtilという「
ユーティリティクラス」に移動しています。
ユーティリティクラスについては第4章をご覧ください。
◎第3段階:ファサードの適用
第2段階でもずいぶんすっきりしたような気がしますね。
ここではさらにもう一歩踏み込んでみます。
リスト5-Bをご覧ください。
ここには「更新件数が0件だったら、挿入処理を実行する」というルールが潜んでいます。
このルールが共通化されない以上、BookDaoクラスを呼び出すたびにここに書かれたルールを記述する必要があります。
また、BookDaoの生成やコネクションのコミットなども共通化できそうです。
さらに大きな視点で見ると、
リスト5-Bの部分が「書籍の更新・挿入処理」というサービスを実装していると言えます。
このサービス部分をファサードとして抽出することで、「書籍の更新・挿入処理」をシンプルなメソッド呼び出しに置き換えることができます。
処理のファサード化は簡単です。
ファサード化したいコードを、別のクラスのメソッドに移動して、必要な情報を引数として渡すだけです。
ここではまずサービスの仕様を明確化するために、サービスのインターフェースを定義しています(
リスト7)。
リスト7 BookService.java
public interface BookService {
void updateOrInsertBook(Book book);
}
そのサービスを実装するクラスにコードを移動しています(
リスト8)。
リスト8 BookServiceImpl.java
public interface BookServiceImpl implements BookService {
public void updateOrInsertBook(Book book) {
BookDao bookDao = new BookDao();
int result = bookDao.update(book);
if (result == 0) {
bookDao.insert(book);
}
bookDao.insert(book);
}
}
リスト9のUpdateBookAction3では
@のようにBookServiceのupdateOrInsertBook()メソッドを呼び出すだけです(図7〜8)。
リスト9 UpdateBookAction3.java
public class UpdateBookAction3 extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest req, HttpServletResponse res)
throws Exception {
UpdateBookForm bookForm = (UpdateBookForm) form;
Book book = new Book();
BeanUtils.copyProperties(book, form);
BookService service = new BookServiceImpl();
service.updateOrInsertBook(book);
return mapping.findForward("success");
}
}
この変更により「書籍の更新・挿入」サービスをどこからでも簡単に呼び出すことができます。
アプリケーションが提供する機能はBookServiceを見れば一目瞭然、明確に知ることができます。
今後、「書籍の検索」処理が追加されたら、BookServiceにメソッドを追加して、BookServiceImplに実装コードを書くというルールを保っていけば、常にアプリケーションが提供する機能をBookServiceで把握していくことができることでしょう。
◎ファサード本来の目的
今回のサンプルでは「サービスを提供するレイヤ」を実現するためにファサードパターンを使用しましたが、
ファサードパターンの本来の目的は複雑なクラス・API呼び出しを実行するための、シンプルな入り口を用意する
ことです。
◎サービスインタフェースを作ることのメリット
サービスインタフェースを作るのがめんどくさいと思われる方も多いことでしょう。
サービスインタフェースを作るメリットは次のような点があります。
- @ルールの明確化
- Aモックオブジェクトが使える
@のルールは、「アクションクラスからの業務処理の呼び出しはサービスインタフェース経由でのみ行うこと」
というシンプルなものになります。
ルールがないと、アクションクラスから直接BookDaoを呼び出したいという誘惑には勝てず、サービスレイヤを構築することは難しいはずです。
Aの「モックオブジェクト」とは、テスト用の仮の実装を持たせたオブジェクトのことです。
BookDaoやBookServiceの実装がまだない状態の時に、BookServiceの仮の実装をモックオブジェクトとして用意することで、BookDaoやBookServiceの実装を待たずにアクションクラスや画面の実装・テストができるようになります。
これは、業務処理とアクションを含めた画面周りの処理を平行して実装・テストするときに役に立つテクニックです
(
リスト10)。
リスト10 MockBookService.java
public class MockBookService implements BookService {
public void updateOrInsertBook(Book book) {
}
}
◎こんなときにも使える
ファサードは複雑なAPIを定期的なコードで呼び出す必要がある場面で有効です。
たとえばJavaMailを使ったメール送信は、割と複雑なAPIを使って定期的なコードを書く必要があり面倒です。
このように面倒だと感じる部分はファサード化が適用できないか検討してみましょう(
リスト11)。
リスト11 MailFacade.java
public class MailFacade {
public void send(
String subject, String to, String from, String message) {
}
}
◎ユーティリティークラスとどう違うの?
乱暴に言ってしまえば、ファサードと
ユーティリティークラスはコードを共通化している点で似ています。
ただし、ユーティリティークラスはさまざまな場所で利用する共通的なコードをまとめたものです。
ファサードはもう少し大きな範囲で、複雑なAPIを適切な順番で実行するための、シンプルな入り口を作るという目的があります。