Template Methodパターンの例
デザインパターンは強力なツールです。強力なツールには常に誤用の可能性があります。注意を怠ると、ぐちゃぐちゃで何だかよくわからないものを作ってしまったりする危険性があります。
Template Methodは、2つ以上の関連するクラスの間での重複をなくすための、1つの解決策を提供するデザインパターンです。これはGammaらによる『Design Patterns』(1995年)で取り上げられていた23個のパターンのうちの1つです。
Template Methodパターンはリファクタリングの目標と考えるのがよいでしょう。本稿では、この目標に向けてコードを手直しする方法を、具体的な例を見ながら説明していきます。「Template Methodパターンとは何ぞや」ではなく、実際にリファクタリングを行い、その結果について語ることにします。
今回のサンプルでは、スキャンしたフォームを管理するためのルールベースのワークフローシステムを構築します。最初に取り組むルールでは、フォーム上で指定されたステートが、引数として渡されるステートと一致していることが求められます。このルールを表すCheckStateRule
クラスでは、次のようなRuleインターフェースを実装します。
import java.util.*; public interface Rule { boolean eval(Case c, List<String> val, boolean returnTrue); }
ワークフローシステムはルールを実行します。ルールを実行することにより、ワークフローの中でフォームを動かすことができます。
CheckStateRule
クラスのコードは単純です。ルールエンジンはeval
メソッドにCase
オブジェクトと引数リストと否定フラグを渡します。このCase
オブジェクトはフォームデータを間接的に含んでいます。switchReturnValues
メソッドは、少し分かりにくい部分ですが、誰かがワークフローシステム内でそれを否定に設定している場合はルールの結果を反転させます。
import java.util.*; /** * * Check if the state the case is located in * is equal to argument passed in. */ public class CheckStateRule implements Rule { private static final String EXCEPT = "Exception: "; String errStr; boolean hasMissingData; public boolean eval(Case c, List<String> val, boolean returnTrue){ boolean returnVal = false; String argVal = null; try { if (val == null || val.size() != 1) { errStr = "Can't convert value list to a state"; hasMissingData = true; return false; } else { argVal = (String)val.get(0); } // get the location object off the form // and get the state the case resides in Location loc = (Location)c.getFormComposite("location"); if (loc == null) { errStr = "Cannot determine the state this case is located in."; hasMissingData = true; return false; } String st = loc.getAttribute("state"); String state = (st == null ? null : st.toUpperCase()); if (state == null) { errStr = "Cannot determine the state this case is located in."; hasMissingData = true; return false; } else if (state.equals(argVal)) { returnVal = true; } // return value List<Object> returns = switchReturnValues(returnTrue, returnVal, "Case does not reside in '" + argVal + "'", "Case resides in '" + argVal + "'"); returnVal = ((Boolean)returns.get(0)).booleanValue(); errStr = (String)returns.get(1); } catch (Exception e) { errStr = EXCEPT + e.getMessage(); hasMissingData = true; return false; } return returnVal; } public List<Object> switchReturnValues(boolean returnTrue, boolean returnVal, String falseMsg, String trueMsg) throws Exception { if (!returnTrue) returnVal = !returnVal; String retErrStr = null; if (!returnVal) { if (returnTrue) retErrStr = falseMsg; else retErrStr = trueMsg; } ArrayList<Object> returns = new ArrayList<Object>(); returns.add(new Boolean(returnVal)); returns.add(retErrStr); return returns; } public boolean isMissingData() { return hasMissingData; } public String getError() { return errStr; } }
CheckStateRule
クラスができたら、続いて2番目のルールであるCheckDependentsRule
クラスに着手します。このルールの機能は、フォーム上で指定された人物が間違いなく姓と名の両方を持つようにすることです。
import java.util.*; /** * * Ensure all dependent detail is supplied */ public class CheckDependentsRule implements Rule { private static final String EXCEPT = "Exception: "; private String errorMessage; private boolean missingData = false; public boolean eval(Case c, List<String> val, boolean returnTrue){ boolean returnVal = false; try { // get the location object off the form // and get the state the case resides in Subscriber sub = (Subscriber)c.getFormComposite("subscriber"); if (sub == null) { errorMessage = "Unable to obtain subscriber info."; missingData = true; return false; } returnVal = true; for (int i = 1; i <= Subscriber.DEPENDENT_ROWS; i++) { if (!hasCompleteDependentName(sub, i)) { returnVal = false; break; } } boolean returnVal1 = returnVal; if (!returnTrue) returnVal1 = !returnVal1; String retErrStr = null; if (returnVal1 == false) { if (returnTrue) retErrStr = "Dependent info incomplete"; else retErrStr = "All dependent info provided"; } List<Object> res = new ArrayList<Object>(); res.add(new Boolean(returnVal1)); res.add(retErrStr); // return value List<Object> returns = res; returnVal = ((Boolean)returns.get(0)).booleanValue(); errorMessage = (String)returns.get(1); } catch (Exception e) { errorMessage = EXCEPT + e.getMessage(); missingData = true; return false; } return returnVal; } private boolean hasCompleteDependentName(Subscriber sub, int number) { String firstName = sub.getAttribute("dep" + number + "first"); String lastName = sub.getAttribute("dep" + number + "last"); if ((!isEmpty(firstName) && isEmpty(lastName)) || (!isEmpty(lastName) && isEmpty(firstName))) { errorMessage = "Complete name must be specified for dependent " + number; return false; } return true; } private boolean isEmpty(String string) { return string == null || string.equals(""); } public boolean isMissingData() { return missingData; } public String getError() { return errorMessage; } }