サンプル
DataAccessMethodパターンとは一体どのようなパターンなのかを具体的に解説します。
下のLoginLogic
を見てください。このクラスはログイン処理に関するビジネスロジックに特化したクラスです。これに対してLoginDam1
またはLoginDam2
はログイン処理に必要なデータアクセスロジックに特化したクラスです。関心事の異なるロジックを持つこの2つのクラスはhookメソッドのfindUser
により結合しています。
設計の手順としては次のようになります。
- BusinessObjectである
LoginLogic
を定義する。 - DBを定義する。
- 必要なら、DataAccessObjectを自動生成する。
- DataAccessMethodである
LoginDam1
またはLoginDam2
を定義する。
重要なのはBusinessObjectがDB定義の前にコンパイルできる事です。BusinessObjectはDB設計の進捗に依存しません。これによりDB設計の納期を引き伸ばす事が出来ます。あるいは、DBが開発の途中で変更になっても、プロジェクトは柔軟な対応をする事が出来ます。
public abstract class LoginLogic { // 粒度の荒いデータアクセスロジックの抽象メソッド protected abstract User findUser(int userId) throws Exception; // クライアントに公開しているメソッド public void doLogic(Request req, Response res) throws Exception{ String userId=req.getParam("userId"); String password=req.getParam("password"); User user = findUser(userId); if(password.equals(user.getPassword())) { user.setPassword(null); res.getSession().setObject("user",user); } } } public final class LoginDam1 extends LoginLogic { // DataAccessObjectによる実装 protected final User findUser(String userId) throws Exception { UserMstDao dao = (UserMstDao)DaoFactory.get("LoginDam#findUser(String)"); User user = dao.findUserByUserId(new Integer(userId)); return user; } } public final class LoginDam2 extends LoginLogic { // JDBCによる実装 protected final User findUser(String userId) throws Exception { Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); Connection conn = DriverManager.getConnection("jdbc:odbc:dam"); try { Statement st = conn.createStatement(); try { String sql = "select * from user_mst"; sql += " where user_id = " + userId; ResultSet rs = st.executeQuery(sql); try { if (rs.next()) { User user = new User(); user.setUserId(userId); user.setPassword(rs.getString("password")); user.setUserNm(rs.getString("user_nm")); return user; }else{ return null; } } finally { rs.close(); } } finally { st.close(); } } finally { conn.close(); } } }
LoginLogic
のデータアクセス処理はLoginDam1
/LoginDam2
に委譲しています。エントリポイントはfindUser
となっています。LoginLogic
のコード例から判るように、DataAccessMethodパターンではBusinessObjectは簡単な処理しか記述されておらず、設計書をそのままトレースしたようなコードになっています。実装というより設計に近いかもしれません。
ビジネスロジックのみが記述されているLoginLogic
のテストは非常に簡単です。スタブの定義に多少の手間はかかるものの、テストで最も厄介なDBの設定を行う必要がないので作業は楽になります。
public final class LoginDamStub extends LoginLogic { protected final User findUser(String userId) throws Exception { User user = new User(); user.setUserId(userId); user.setUserNm("小泉純一郎"); user.setPassword("99999"); return user; } }
そこで、このイテレーションを2つに分割します。最初のイテレーションではLoginDamStub
とLoginLogic
の開発と検収を以って完了とします。ここではLoginLogic
のテストを十分に行います。次のイテレーションではLoginDam1
/LoginDam2
の開発と検収を以って完了とします。ここではfindUser
のテストを十分に行います。
- BusinessObjectである
LoginLogic
を定義する。 - Stubである
LoginDamStub
を定義する。 - BusinessObjectである
LoginLogic
を検収する。 - ファーストイテレーションを完了する。
- DBを定義する。
- 必要なら、DataAccessObjectを自動生成する。
- DataAccessMethodである
LoginDam1
またはLoginDam2
を定義する。 - DataAccessMethodである
LoginDam1
またはLoginDam2
を検収する。 - セカンドイテレーションを完了する。
DataAccessMethodパターンはGoFのTemplateパターンを応用した仕掛けで、Strategyパターンに比べて開発効率が高く実践的パターンと言えます。Strategyパターンも悪くないのですが分析や設計にコストがかかるという欠点があり、これを怠るとむしろ足枷になってしまうというリスクがあります。
LoginDam2
はDBに直接依存していますが、LoginLogic
はDBに直接依存しないので単体試験が簡単であることが判ります。LoginDam1
は直接依存していないものの自動生成されたDataAccessObjectはDB仕様に直結しているのでLoginDam2
と殆ど同じです。
ここで、DBに突然修正が入るケースを考えてみましょう。
例えば「user_mst」テーブルが「emp_mst」テーブルに、「user_id」カラムが「emp_id」カラムに変わった場合、DataAccessMethodであるLoginDam1
/LoginDam2
に下のような修正が入ります。
public final class LoginDam1 extends LoginLogic { // DataAccessObjectは粒度の細かいデータアクセスロジックとして // 定義しているのでLoginDamはDB変更の影響を受けやすいが、 // このクラスでその変更は吸収されるのでLoginLogicを修正しないですむ。 protected final User findUser(String userId) throws Exception { EmpMstDao dao = (EmpMstDao)DaoFactory.get("LoginDam#findUser(String)"); Emp emp = dao.findUserByLoginId(new Integer(userId)); User user = new User(); user.setUserId(userId); user.setUserNm(emp.getUserNm()); user.setPassword(emp.getPassword()); return user; } } public final class LoginDam2 extends LoginLogic { // JDBCによる処理でもDAOクラスを利用した場合と同様の解決ができる。 protected final User findUser(String userId) throws Exception { Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); Connection conn = DriverManager.getConnection("jdbc:odbc:dam"); try { Statement st = conn.createStatement(); try { String sql = "select * from emp_mst where emp_id = " + userId; ResultSet rs = st.executeQuery(sql); try { if (rs.next()) { User user = new User(); user.setUserId(userId); user.setPassword(rs.getString("password")); user.setUserNm(rs.getString("emp_nm")); return user; }else{ return null; } } finally { rs.close(); } } finally { st.close(); } } finally { conn.close(); } } }
このような変更は多くのプロジェクトでしばしば発生します。このときBusinessObjectが直接DataAccessObjectやSQLを扱っていれば、両者は結合度が高くなりBusinessObjectの修正にコストがかかります。
しかしLoginDam1
やLoginDam2
のようなBoundaryObjectを介する事でDBの仕様が変更してもBusinessObjectは全く修正する必要がなくなります。つまりファーストイテレーションで行ったテストを無駄にせずに済みます。これは非常に大きな利点です。
その他の解決
継承ではなく委譲
DB変更に対してBoundaryObjectが有効であることはお分かりいただけたと思います。ここで継承ではなく委譲による設計も説明しておきます。委譲によるクラス設計はクラス数を増大してしまいますが、それを補って余りある高い柔軟性があります。ただし、プロジェクトで委譲によるBoundaryObjectが可能なのは
- クラス数が増えてもそれを管理する体制が整っている。
- DAOジェネレータの専門家がプロジェクトに参加している。
という2つの条件が揃っている場合です。
この条件がクリアされたプロジェクトで委譲のBoundaryObjectを使う場合、SpringのようなDIコンテナを存分に活用する必要があります。BusinessObjectやDataAccessMethodだけでなくBoundaryObjectも設定ファイルにより制御できます。つまり、後工程で仕様変更や不具合が発生した時に最小限のコストで対応する事が出来るのです。
BusinessObjectとDataAccessMethodの特性
ここで、それぞれのクラスの特性について考えてみましょう。
BusinessObjectはデータアクセスに関する知識がない者でも実装でき、DataAccessMethodはビジネスの知識がない者でも実装できます。そしてBoundaryObjectはビジネスとデータアクセスのほんの少しの知識があれば実装できます。
これらの特性を生かすことで非常に柔軟な設計が可能となります。分割されたより小さな機能をもつコンポーネントをフレームワークにプラグインし、運用によりそれを制御することができます。ビジネス層以降は3階層になり担当は細分化されます。担当を決める事で開発者は深い専門的知識を身に着ける機会を得ます。適切な技術者に担当作業を振り分けることで開発効率と品質は向上します。
IntelligentDataGatewayパターン
委譲のBoundaryObjectはTableDataGateway(PofEAAカタログではDataAccessObjectパターンのことをTableDataGatewayパターンと呼びます)から取得したTableData(生データ)をIntelligentData(編集されたデータ)に変換してビジネスロジックに引き渡す役割を持ちます。
そこで、この委譲のBoundaryObjectをIntelligentDataGateway、このクラスを使ったパターンをIntelligentDataGatewayパターンと命名しておきます。DataAccessMethoodパターンが継承を使っているのに対しIntelligentDataGatewayパターンは委譲を使っています。委譲の方が柔軟性は高いといえます。
ただし、残念ながら先ほど説明した2つの条件をプロジェクトがクリアすることは非常に難しいと言えます。筆者としてはIntelligentDataGatewayパターンよりDataAccessMethodパターンをお奨めします。
進捗遅延対策
皆さんは設計者でしょうか管理者でしょうか。管理者の経験がある方なら、納期が迫っているのに進捗がかなり遅れているという状況で、「人を投入して何とかなるならそうするのに…」と思った経験もあるのではないでしょうか。進捗が致命的に遅れている状況で人員をどれほど投入しても業務把握に時間がかかるため、無計画な人員投入はそれほど効果を期待できません。
このような事態に遭遇したらDataAccessMethodパターンを思い出してください。BusinessObjectはビジネスの知識習得が必要なので支援が難しいクラスですがDataAccessMethodのようなビジネスの知識が不要なクラスは火消し役にうってつけです。DataAccessMethodパターンでは、BusinessObjectとDataAccessMethodを同一人物が実装する必要はまったくありません。ですから、開発遅延が発生した場合にも、BusinessObjectは従来のメンバーに、DataAccessMethodは火消しメンバーに別々にやってもらう事で、追加作業員投入による効果が大いに期待できるのです。
パターン採用の注意
デザインパターンのプラクティス
DataAccessMethodパターンに限らずGoFやCJ2EEのようなデザインパターンやそれに伴うツールの導入時には開発プロセスあるいは構成管理のカスタマイズを前提とするべきです。デザインパターンは優良で再利用可能な『設計方式』ですが、その効果を最大限にするにはそのパターン特有の『プラクティス』を取り込む必要があります。
例えばFacadeパターンでは「Facade担当者に特殊APIによる開発を任せる」というFacadeパターン特有のプラクティスがあります。TemplateMethodパターンでは「TemplateMethod担当者に複雑な機能、HookMethod担当者に単機能を担当させる」というTemplateMethodパターン特有のプラクティスがあります。こうしたプラクティスを適切に取り込めなければ、デザインパターン導入の効果を得られなくなるどころか導入により逆にプロジェクトを混乱させる事態になるかもしれません。
開発プロセスを従来のままにデザインパターンのような高度なノウハウを導入すれば、副作用が発生するのは自明なのです。これを避けるには、プロジェクト早期にデザインパターン導入を受け入れるにはどのようにプロセスをカスタマイズすれば良いかを十分に検討しなければなりません。それでも、導入した最初のプロジェクトからうまく運用されるのは稀なのです。
さらに、デザインパターンとそのプラクティスは暗黙知であり、完全な文書化は困難です。成功させるにはデザインパターンを繰り返し適用し続け、組織内に『~の匠』のような熟練工を育てていくしかないのです。
暗黙知確保
そのために、まずは自社システムなどで試運転をし成功実績をあげるのがいいでしょう。これによりある程度の暗黙知を確保します。
ソフトハウスがデザインパターンを採用する場合には、少なくとも自社システムのようなある程度は失敗が多めにみてもらえる開発で試運転できる機会を持つべきです。それをせずにお客様のシステム開発でいきなり適用して失敗でもすれば、いくら予算がなかったからと言っても言い訳になりません。ノウハウもないのにお客様の案件で新しい方式を導入するのは論外です。既存の成功実績のある開発方式で要件が満たせないなら、その案件は請けるべきではないのです。
また、カスタマイズした開発プロセスや構成管理が安定するまでの期間の長さは、デザインパターンの特性、開発時の環境(自社または協力会社の体質やPM/SEの能力など)により変動します。こうしたことも考慮に入れてどんなデザインパターンを選定するか検討すべきでしょう。流行っているからという導入理由も企業競争という点である程度は正当化されますが、再利用が見込めないデザインパターンは導入しても組織に利益をもたらしません。
おわりに
いかがだったでしょうか。実装の話がメインではなかったので、開発を中心に仕事をされている方にとっては退屈だったかもしれません。
実は、筆者の能力不足から前回十分に本パターンの説明をできなかったためにDataAccessMethodパターンが十分理解されていないことを知りました。そこで今回も設計の話の続きを書かせていただきました。ご質問等はメッセージをいただければ回答致します。次回は総仕上げとしてサンプルプログラムによる実装の詳細解説をさせていただきたいと思います。
プロジェクトにおけるDataAccessMethodパターンの利点をまとめます。
- 【高凝集度】メンバーにあわせて作業を割り振ることができる。
- 【低結合度】仕様変更に柔軟に対応できる。
このパターンが皆さんのプロジェクトに幸せをもたらすことを願っています。