デザインパターン - Facade

把复杂的调用处理集中到Facade类中,对外仅提供facade的简单的接口,避免过多的不必要的参数及方法的暴露,让调用侧更方便使用。

 

Facade (ファサード)

 
イントロダクション
パターン解説
具体的な例
まとめ
 

イントロダクション

 みなさんのサーブレットStrutsを使用している場合はアクションクラス)の行数は、平均どれくらいでしょうか? データベースアクセスや業務処理など、すべての処理をサーブレットに詰め込もうとして、あっという間に1000行を越すような「太ったサーブレット」を作ってしまったことありませんか? サーブレットを初めて書いたときは筆者もそうでした。
 このような「長く」「すべての処理が入った」サーブレットのことをすべてのことを行う魔法のようなサーブレットということで「マジックサーブレット」と呼びます。 マジックサーブレットは保守や機能拡張が難しいのはもちろんのこと、「アプリケーションが提供する機能」を把握することが難しくなるという弊害があります。 機能を把握できないと「あの機能ってどこにあったっけ?」という状況を生み出しがちになります。
 そのような状況を避けるためにも、「サービスを提供するレイヤー(層)」をはっきりとさせ、アプリケーションの見通しをよくしておくことは大切なことです。
 本節では、マジックサーブレットに機能分割のリファクタリングを施していくことで、Webアプリケーションに「サービスを提供するレイヤ」を持たせる方法を解説します。
 具体的には「ファサード」というパターンを使います。ファサードは複雑な処理の呼び出しを単純化するためのパターンです。
 マジックサーブレットの長くて複雑な業務処理は別のクラスに移動させて、サーブレットからは「業務処理を呼び出すだけ」という構成にしていきます。
 今回ご紹介する「サービスを提供するレイヤ」を作るためのファサードパターンは、J2EEパターンではセッションファサードパターンと呼ばれています。 セッションファサードEJBのセッションBeanを対象としたパターンです。
 本特集ではEJBを対象としないため、セッションファサードという名前は使用せず、単にファサードと呼んでいますが、パターンの目的・手法はほとんど同じです。

パターン解説

 ファサードは「建物の正面」という意味です。
 筆者はファサードのことを考えるときは大きな建物の「シンプルな正面玄関」をイメージしています。 「シンプルな正面玄関」の奥には複雑な処理が隠されていますが、呼び出す側は「シンプルな正面玄関=シンプルなメソッド」だけを意識すればよいわけです(図5)。

図5 ファサードパターンのイメージ

 まずは簡単な例を見てみましょう。 図6の左側のdoGet()メソッドは「パラメータの取得」「データベース処理」「業務処理」などが渾然一体となって、かなり複雑でマジックなサーブレットと化しています。 図6の右側のように複雑な業務処理の部分を「ファサード」クラスに移動・分割することで、サーブレットからは「ファサードを呼び出すだけ」の状態にすることができます。

図6 ファサードパターン適用前/適用後

具体的な例

 より具体的な例を見てみましょう。
 サンプルは、シンプルな書籍データの更新・挿入処理を行うものです。 今回はサーブレットでなくStrutsのアクションクラスを題材にしています。 Strutsのアクションクラスも役割としてはサーブレットとまったく同じですので、複雑な「マジックアクション」は極力避けるべきです。
 ここでは、「マジックアクションの状態」から、ファサードにより「アプリケーションの提供する機能が分割された状態」にリファクタリングを施していきます。

MEMO
Strutsアクションクラスの役割
 Strutsのアクションクラスの役割はWebコントローラと呼ばれます。
 WebコントローラはWeb特有の処理を行い、業務処理との橋渡しを行うのが目的です。
 つまりリスト4-⑤「次画面への遷移」やリスト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();

			// ③更新件数が0なら挿入処理
			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(); }  …④
		}

		return mapping.findForward("success");  …⑤
	}
}

 execute()メソッドでは以下の処理が行われています。

①コネクションの取得
②更新SQLの発行
③更新件数が0だった場合、挿入処理とみなして、挿入のSQLを発行
④コネクションを閉じる
⑤次画面への遷移

 このうち①~④までが更新・挿入処理に当たりますが、アクションクラスに直接処理が埋め込まれているため、他 のクラスからこの機能を呼び出すことはできません。
 そこで、データベースにアクセスするクラスは後述のDAO(データアクセスオブジェクト)に切り出してみたいと思います。

◎第2段階:データベースにアクセスするコードを別クラスに分割

 データベースにアクセスするコードを別クラスに分割したものがリスト5リスト6です。

リスト5 UpdateBookAction2.java

/**
 * 書籍データ更新・挿入アクション(DAOを分割)です。
 */
public class UpdateBookAction2 extends Action {
	/**
	 * 書籍の更新処理を行います。
	 */
	public ActionForward execute(ActionMapping mapping, ActionForm form,
			HttpServletRequest req, HttpServletResponse res)
						throws Exception {

		UpdateBookForm bookForm = (UpdateBookForm) form;

		// ①フォームBeanからドメインオブジェクトにデータをコピー
		Book book = new Book();
		BeanUtils.copyProperties(book, form);  …
		// 更新処理
		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();
	}

	/**
	 * 更新処理を行います。
	 * @param book書籍データ
	 * @return 更新件数
	 */
	 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);
	 	}
	 }

	/**
	 * 挿入処理を行います。
	 * @param book書籍データ
	 * @return 更新件数
	 */
	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);
	 	}
	 }

	/**
	 * Connectionをコミットします。
	 */
	public void commit() {
		sqlUtil.commit(conn);
	}
}

 コネクションの取得、SQLの発行などをBookDaoクラスに移動しました。 たったこれだけで、アクションクラスはすっきり見やすくなりました。
 この変更でBookDaoクラスはUpdateBookAction2以外のアクションクラスから呼び出すことができるようになりました。
 その他変更点としてはリスト5-②Jakarta CommonsのJavaBeans操作用コンポーネントであるBeanUtils を使って、UpdateBookFormからBookオブジェクトにデータのコピーをおこなっています。 このことでBookDaoにStruts固有のクラスであるフォームBeanを渡さないようにしています。 「別にUpdateBookFormを直接渡してもいいんじゃないの?」という声が聞こえてきそうですが、UpdateBookFormを直接渡すとBookDaoはUpdateBookFormに極度に依存します。
 この業務専用のBookオブジェクトをきちんと作ってを渡すことで、BookDaoはさまざまな場所で利用できる「汎用性の高い」クラスになります。
 またコネクションの取得、クローズなどの共通的な処理はSqlUtilという「ユーティリティクラス」に移動しています。 ユーティリティクラスについては第4章をご覧ください。

◎第3段階:ファサードの適用

 第2段階でもずいぶんすっきりしたような気がしますね。
 ここではさらにもう一歩踏み込んでみます。リスト5-③をご覧ください。
 ここには「更新件数が0件だったら、挿入処理を実行する」というルールが潜んでいます。 このルールが共通化されない以上、BookDaoクラスを呼び出すたびにここに書かれたルールを記述する必要があります。 また、BookDaoの生成やコネクションのコミットなども共通化できそうです。 さらに大きな視点で見ると、リスト5-③の部分が「書籍の更新・挿入処理」というサービスを実装していると言えます。
 このサービス部分をファサードとして抽出することで、「書籍の更新・挿入処理」をシンプルなメソッド呼び出しに置き換えることができます。
 処理のファサード化は簡単です。 ファサード化したいコードを、別のクラスのメソッドに移動して、必要な情報を引数として渡すだけです。
 ここではまずサービスの仕様を明確化するために、サービスのインターフェースを定義しています(リスト7)。

リスト7 BookService.java

/**
 * 書籍管理サービスインタフェースです。
 */
public interface BookService {
	/**
	 * 書籍データの更新・挿入処理を行います。
	 * 更新処理でデータが1件も更新されなかった場合、
	 * 新規データとみなして挿入処理を行います。
	 * @param book書籍データ
	 */
	 void updateOrInsertBook(Book book);
}

そのサービスを実装するクラスにコードを移動しています(リスト8)。

リスト8 BookServiceImpl.java

/**
 * 書籍管理サービスインタフェース実装クラスです。
 */
public interface BookServiceImpl implements BookService {
	public void updateOrInsertBook(Book book) {
		// DAOクラスの生成
		BookDao bookDao = new BookDao();
		// 更新処理
		int result = bookDao.update(book);
		// 更新件数が0なら挿入処理
		if (result == 0) {
			// 挿入処理
			bookDao.insert(book);
		}
		bookDao.insert(book);
	}
}

 リスト9のUpdateBookAction3ではのようにBookServiceのupdateOrInsertBook()メソッドを呼び出すだけです(図7~8)。

図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;

		// FormBeanからドメインオブジェクトにデータをコピー
		Book book = new Book();
		BeanUtils.copyProperties(book, form);
		// ①更新 or 挿入処理
		BookService service = new BookServiceImpl();
		service.updateOrInsertBook(book);

		return mapping.findForward("success");
	}
}

 この変更により「書籍の更新・挿入」サービスをどこからでも簡単に呼び出すことができます。
 アプリケーションが提供する機能はBookServiceを見れば一目瞭然、明確に知ることができます。
 今後、「書籍の検索」処理が追加されたら、BookServiceにメソッドを追加して、BookServiceImplに実装コードを書くというルールを保っていけば、常にアプリケーションが提供する機能をBookServiceで把握していくことができることでしょう。

まとめ

ファサード本来の目的

 今回のサンプルでは「サービスを提供するレイヤ」を実現するためにファサードパターンを使用しましたが、 ファサードパターンの本来の目的は複雑なクラス・API呼び出しを実行するための、シンプルな入り口を用意する ことです。

◎サービスインタフェースを作ることのメリット

 サービスインタフェースを作るのがめんどくさいと思われる方も多いことでしょう。 サービスインタフェースを作るメリットは次のような点があります。

①ルールの明確化
②モックオブジェクトが使える

 ①のルールは、「アクションクラスからの業務処理の呼び出しはサービスインタフェース経由でのみ行うこと」 というシンプルなものになります。
 ルールがないと、アクションクラスから直接BookDaoを呼び出したいという誘惑には勝てず、サービスレイヤを構築することは難しいはずです。
 ②の「モックオブジェクト」とは、テスト用の仮の実装を持たせたオブジェクトのことです。
 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を適切な順番で実行するための、シンプルな入り口を作るという目的があります。