SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine(コードジン) DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

デザインパターンの使い方

デザインパターンの使い方: Template Method

Template Methodでクラス間の重複をなくす

抽象化による弊害

 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パターンに向けてリファクタリングを行う場合もあれば、それが最善の解決策でないと気付く場合もあります。

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
デザインパターンの使い方連載記事一覧

もっと読む

この記事の著者

japan.internet.com(ジャパンインターネットコム)

japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.comEarthWeb.com からの最新記事を日本語に翻訳して掲載するとともに、日本独自のネットビジネス関連記事やレポートを配信。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

Jeff Langr(Jeff Langr)

本格的なソフトウェアの開発に四半世紀以上携わってきたベテランのソフトウェア開発者。『Agile Java: Crafting Code With Test-Driven Development』(Prentice Hall、2005年)と、他の1冊の著書がある。『Clean Code』(Uncle Bob Martin著、Prentice Hall、2008年8月)にも寄稿している。また、ソフトウェア開発に関する記事を80件以上執筆しており、そのうちの...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/2819 2008/08/22 15:54

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー