抽象化による弊害
CheckStateRuleクラスのevalメソッドとevalWithThrowメソッドは、ポリシーの定義を提供しています。もちろん、これらの変更は段階的に行います。途中で単純ミスを犯すこともあるからです。絶えずテストを行えば、こうしたミスが大きな問題に発展するのを防ぐことができます。
これで、evalメソッドとevalWithThrowメソッドをBaseRuleクラスに移すことができます。ただし、そうするとコンパイルエラーが生じます。BaseRuleクラスをコンパイルできるようにするため、CheckStateRuleクラスに固有のアイテム(例えばextractFormData)のためのスタブを作成します。それらのスタブをBaseRuleクラスとCheckStateRuleクラスの両方でprotectedに変更します。ここで改めてテストを実行します。
public boolean eval(Case c, List<String> val, boolean returnTrue) { try { return evalWithThrow(c, val, returnTrue); } catch (Exception e) { errorMessage = BaseRule.EXCEPT + e.getMessage(); hasMissingData = true; return false; } } private boolean evalWithThrow(Case c, List<String> val, boolean returnTrue) { if (!extractArgs(val)) return false; if (!extractFormData(c)) return false; boolean returnVal = executeRule(c); return prepareReturnValues(returnTrue, returnVal); } protected boolean prepareReturnValues(boolean returnTrue, boolean returnVal) { return false; } protected boolean executeRule(Case c) { return false; } protected boolean extractFormData(Case c) { return false; } protected boolean extractArgs(List<String> val) { return false; }
次のステップで、このスタブメソッドを抽象メソッドに変換します(これでBaseRuleクラスが抽象クラスになります)。この変更によって、CheckDependentsRuleクラスでコンパイルエラーが生じます。すべてのメソッドを同時に抽象メソッドとして宣言するのではなく、一度に1つずつ行ってください(その方がかえってスムーズに進むでしょう)。
終了したら、思い切ってCheckDependentsRuleクラスのevalメソッドを削除します。その結果、コードは次のようになります。
import java.util.*; /** * * Ensure all dependent detail is supplied */ public class CheckDependentsRule extends BaseRule implements Rule { private Subscriber sub; @Override protected boolean prepareReturnValues(boolean returnTrue, boolean returnVal) { List<Object> returns = switchReturnValues(returnTrue, returnVal, "Dependent info incomplete", "All dependent info provided"); returnVal = ((Boolean)returns.get(0)).booleanValue(); errorMessage = (String)returns.get(1); return returnVal; } @Override protected boolean executeRule(Case c) { for (int i = 1; i <= Subscriber.DEPENDENT_ROWS; i++) { if (!hasCompleteDependentName(sub, i)) { return false; } } return true; } 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; } @Override protected boolean extractFormData(Case c) { sub = (Subscriber)c.getFormComposite("subscriber"); if (sub == null) { errorMessage = "Unable to obtain subscriber info."; hasMissingData = true; return false; } return true; } @Override protected boolean extractArgs(List<String> val) { return true; } private boolean isEmpty(String string) { return string == null || string.equals(""); } }
最終的なBaseRuleクラスは次のようになります。
import java.util.*; abstract public class BaseRule { protected String errorMessage; protected boolean hasMissingData = false; static final String EXCEPT = "Exception: "; protected List<Object> switchReturnValues(boolean returnTrue, boolean returnVal, String trueMsg, String falseMsg) { boolean returnVal1 = returnVal; if (!returnTrue) returnVal1 = !returnVal1; String retErrStr = null; if (returnVal1 == false) { if (returnTrue) retErrStr = trueMsg; else retErrStr = falseMsg; } List<Object> res = new ArrayList<Object>(); res.add(new Boolean(returnVal1)); res.add(retErrStr); return res; } public boolean eval(Case c, List<String> val, boolean returnTrue) { try { return evalWithThrow(c, val, returnTrue); } catch (Exception e) { e.printStackTrace(); errorMessage = BaseRule.EXCEPT + e.getMessage(); hasMissingData = true; return false; } } private boolean evalWithThrow(Case c, List<String> val, boolean returnTrue) { if (!extractArgs(val)) return false; if (!extractFormData(c)) return false; boolean returnVal = executeRule(c); return prepareReturnValues(returnTrue, returnVal); } abstract protected boolean prepareReturnValues(boolean returnTrue, boolean returnVal); abstract protected boolean executeRule(Case c); abstract protected boolean extractFormData(Case c); abstract protected boolean extractArgs(List<String> val); public boolean isMissingData() { return hasMissingData; } public String getError() { return errorMessage; } }
最終的なCheckStateRuleクラスは次のようになります。
import java.util.*; /** * * Check if the state the case is located in * is equal to argument passed in. */ public class CheckStateRule extends BaseRule implements Rule { private String argVal; private String formState; protected boolean extractFormData(Case c) { // get the location object off the form and get the state the // case resides in Location loc = (Location)c.getFormComposite("location"); if (loc == null) { errorMessage = "Cannot determine the state this case is located in."; hasMissingData = true; return false; } String st = loc.getAttribute("state"); formState = (st == null ? null : st.toUpperCase()); if (formState == null) { errorMessage = "Cannot determine the state this case is located in."; hasMissingData = true; return false; } return true; } protected boolean prepareReturnValues(boolean returnTrue, boolean returnVal) { List<Object> returns = switchReturnValues(returnTrue, returnVal, "Case does not reside in '" + argVal + "'", "Case resides in '" + argVal + "'"); returnVal = ((Boolean)returns.get(0)).booleanValue(); errorMessage = (String)returns.get(1); return returnVal; } protected boolean executeRule(Case c) { return formState.equals(argVal); } protected boolean extractArgs(List<String> val) { if (val == null || val.size() != 1) { errorMessage = "Can't convert value list to a state"; hasMissingData = true; return false; } argVal = (String)val.get(0); return true; } }
さらに若干の変更を行えば、サブクラスをすっかりきれいにすることができます。prepareReturnValuesメソッドを見ると、CheckStateRuleクラスとCheckDependentsRuleクラスの間でいくらか重複がありそうです。この作業は読者の課題とし、ここでは行いません。
evalおよびevalWithThrow内のポリシーメソッドは「テンプレートメソッド」とも呼ばれます。Template Methodパターンを適用する場合、動作のアルゴリズム(テンプレート)を表現する共通のスーパークラスメソッドを用意します。
テンプレートアルゴリズムは完全なものではなく、一部をサブクラス(この例ではCheckDependentsRuleクラスとCheckStateRuleクラス)で変更する必要があります。テンプレートメソッドにはアルゴリズム全体を入れますが、その中に「穴」を残しておきます。「穴」の部分は、ベースクラス上で定義した抽象メソッドの呼び出しという形で実装されます。この穴はサブクラスが埋めなければなりません。
Template Methodパターンのマイナス面は、サブクラスの実装しか見ない人にとっては詳細が分かりにくいという点です。CheckDependentsRuleクラスが何をするのか完全に理解するためには、BaseRuleクラスの設計がどうなっているのか十分に読み取る必要があります。
重複を取り除くと、その代償として抽象化レベルが上がり、理解のコストが上昇します。私の場合、Template Methodパターンを適用するときにはメソッド名を絶えず見直し、この穴をサブクラスでどのように埋めるか分かりやすく示す、最善の方法を考えるようにしています。
Template Methodは、使いすぎて悪い結果を招きやすいパターンの1つです。問題の解決策としてTemplate Methodパターンを考えるよりも、テスト駆動型開発の流れに従うことをお勧めします。私自身も、Template Methodパターンに向けてリファクタリングを行う場合もあれば、それが最善の解決策でないと気付く場合もあります。
