Help us understand the problem. What is going on with this article?

よく使うJavaライブラリで味わうデザインパターン - Template Methodパターン

More than 1 year has passed since last update.

普段よく使うJavaライブラリにも、GoFのデザインパターンが隠されています。日々の作業が忙しく見逃しがちですが、たまにはじっくり一種の芸術ともいえる美しい設計を味わってみましょう。

今回の芸術

ソースファイル

HelloServletクラス
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
...

public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        try (PrintWriter out = response.getWriter()) {
            out.println("<html>");
            out.println("<body>");
            out.println("<h1>Hello, world!</h1>");
            out.println("</body>");
            out.println("</html>");
        }
    }
}

実行結果

HelloWorld.png

サーバーサイドでJavaを使ったことがある人なら一度は目にしたことがあると思われる、Java ServletのHello Worldプログラムです。あらためて見てみると、初歩の初歩からオブジェクト指向を強く主張してくるソースコードに圧倒されそうになります。

鑑賞のポイント

Java Servletでは、HttpServletクラスを継承して、doGetメソッドをオーバーライドするだけで、HTTPのGETリクエストに対応することができます。(XMLファイルには、そのクラスとURLをマッピングさせる記述が必要です。)

上記のコードは、WEBフレームワークを使わずにHTTPレスポンスを返すコードにしては、簡潔にまとまっているように見えます。クラスの継承やメソッドのオーバーライドに秘密がありそうですが、どのような設計になっているのでしょうか? 一緒に探っていきましょう。

Template Methodパターンを使わない場合

冒頭のコードでは、Template Methodパターンが使われています。まず、Template Methodパターンを使わない場合、どのようになるのかを考えてみましょう。

Java Servletでは、HTTPリクエストがあれば、そのURLに対応するServletインターフェイスを実装したクラスのserviceメソッドが呼び出される仕組みになっています。そのため、以下のように、親クラスがない自前のクラスでServletインターフェイスのserviceメソッドを実装しても問題なく動きます。

HelloServletクラス
...
public class HelloServlet implements Servlet {
    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        HttpServletRequest request =  (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // リクエストメソッドで処理を分ける
        String method = request.getMethod();
        if (method.equals(METHOD_GET)) {
            // 最終更新日時のヘッダが付与されているかどうかで処理を分ける
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                // 最終更新日時のヘッダが付与されていない場合
                // HTMLを作成する
                response.setContentType("text/html;charset=UTF-8");
                try (PrintWriter out = response.getWriter()) {
                    out.println("<html>");
                    out.println("<body>");
                    out.println("<h1>Hello, world!</h1>");
                    out.println("</body>");
                    out.println("</html>");
                }
            } else {
                // 最終更新日時のヘッダが付与されている場合
                long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                if (ifModifiedSince < lastModified) {
                    maybeSetLastModified(response, lastModified);

                    // HTMLを作成する
                    response.setContentType("text/html;charset=UTF-8");
                    try (PrintWriter out = response.getWriter()) {
                        out.println("<html>");
                        out.println("<body>");
                        out.println("<h1>Hello, world!</h1>");
                        out.println("</body>");
                        out.println("</html>");
                    }
                } else {
                    // 前回から更新がなければステータスコードのみを返す
                    response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }

        } else if (method.equals(METHOD_HEAD)) {
      ...

このクラスをサーブレットとして登録するために、XMLファイルにURLとクラス名のマッピングを書きます。

web/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
    <servlet>
        <servlet-name>hello</servlet-name>
        <servlet-class>HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>hello</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>30</session-timeout>
    </session-config>
</web-app>

すると、ドメイン名/helloのURLでアクセスすれば、上記のHelloServlet#serviceが呼び出されるようになります。実行結果は冒頭の画像と同じです。

このシステムの機能が"Hello, world!"を表示させるだけなら上記の実装で大丈夫なのですが、新しい機能が必要になると、だんだん苦しくなってきます。試しに、新しい機能を持つByeServletクラスを追加します。

ByeServletクラス
...
public class ByeServlet implements Servlet {
  @Override
  public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
      HttpServletRequest request =  (HttpServletRequest) req;
      HttpServletResponse response = (HttpServletResponse) res;

      // リクエストメソッドで処理を分ける
      String method = request.getMethod();
      if (method.equals(METHOD_GET)) {
          // 最終更新日時のヘッダが付与されているかどうかで処理を分ける
          long lastModified = getLastModified(req);
          if (lastModified == -1) {
              // 最終更新日時のヘッダが付与されていない場合
              // HTMLを作成する
              response.setContentType("text/html;charset=UTF-8");
              try (PrintWriter out = response.getWriter()) {
                  out.println("<html>");
                  out.println("<body>");
                  out.println("<h1>Bye, world!</h1>");
                  out.println("</body>");
                  out.println("</html>");
              }
          } else {
              // 最終更新日時のヘッダが付与されている場合
              long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
              if (ifModifiedSince < lastModified) {
                  maybeSetLastModified(response, lastModified);

                  // HTMLを作成する
                  response.setContentType("text/html;charset=UTF-8");
                  try (PrintWriter out = response.getWriter()) {
                      out.println("<html>");
                      out.println("<body>");
                      out.println("<h1>Bye, world!</h1>");
                      out.println("</body>");
                      out.println("</html>");
                  }
              } else {
                  // 前回から更新がなければステータスコードのみを返す
                  response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
              }
          }

      } else if (method.equals(METHOD_HEAD)) {
      ...
web/WEB-INF/web.xml(追加分)
...
  <!-- 追加分 -->
  <servlet>
      <servlet-name>bye</servlet-name>
      <servlet-class>ByeServlet</servlet-class>
  </servlet>
  <servlet-mapping>
      <servlet-name>bye</servlet-name>
      <url-pattern>/bye</url-pattern>
  </servlet-mapping>
...

ドメイン名/byeのURLでアクセスすれば、ByeServletが動き、ブラウザには次のように表示されます。

ByeWorld.png

ここで、2つのサーブレットHelloServletByeServletを見比べてみましょう。処理の流れは、どちらも次のようになっています。

処理の流れ

  1. リクエストメソッド(GET/POSTなど)で処理を分ける
  2. 最終更新日時のヘッダが付与されていることを確認する
  3. 付与されていなければHTMLを返す
  4. 付与されていて、更新があればHTMLを返す
  5. 付与されていて、更新がなければステータスコードのみを返す

2つのクラスで異なる個所は「HTMLを返す」ところの中身だけで、他はまったく同じです。処理の流れは同じなのにソースコードが重複していて、コピペして使いまわされたような状態になっています。苦しい実装で、美しくありませんね。

Template Methodパターンを使った場合

ここでTemplate Methodパターンを適用してみましょう。

クラス間の共通の処理は親クラスで実装して、異なる処理だけ子クラスで実装します。今回の例では、「共通の処理」は最終更新日時のヘッダによってフローを分けること、「異なる処理」はHTMLを生成することです。

まずは親クラスです。

親クラスの実装(javax.servlet.http.HttpServletクラス)
package javax.servlet.http;
...
public abstract class HttpServlet extends GenericServlet {
    ...
    /**
     * 子クラスでオーバーライドするメソッド
     */
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 子クラスでオーバーライドすることを想定しているので、ここを通過したらエラーにする
    }

    /**
     * テンプレートメソッド
     */
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod();
        if (method.equals(METHOD_GET)) {
            // 最終更新日時のヘッダが付与されているかどうかで処理が変わる
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                // 最終更新日時のヘッダが付与されていない場合
                // HTMLを作成する
                doGet(req, resp);
            } else {
                // 最終更新日時のヘッダが付与されている場合
                long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                if (ifModifiedSince < lastModified) {
                    maybeSetLastModified(resp, lastModified);

                    // HTMLを作成する
                    doGet(req, resp);
                } else {
                    // ステータスコードのみを返す
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }

        } else if (method.equals(METHOD_HEAD)) {
        ...

上記は私が書いたコードではなく、Java Servlet標準のサーブレットクラスjavax.servlet.http.HttpServletそのままのコードです。Java Servletを作る場合、通常は、このクラスを継承して子クラスでHTTPのリクエストメソッド(GET/POSTなど)に対応する処理を実装します。以下のようになります。

子クラスの実装(HelloServletクラス)
...
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // HTMLを作成する
        response.setContentType("text/html;charset=UTF-8");
        try (PrintWriter out = response.getWriter()) {
            out.println("<html>");
            out.println("<body>");
            out.println("<h1>Hello, world!</h1>");
            out.println("</body>");
            out.println("</html>");
        }
    }
}
子クラスの実装(ByeServletクラス)
...
public class ByeServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // HTMLを作成する
        response.setContentType("text/html;charset=UTF-8");
        try (PrintWriter out = response.getWriter()) {
            out.println("<html>");
            out.println("<body>");
            out.println("<h1>Bye, world!</h1>");
            out.println("</body>");
            out.println("</html>");
        }
    }
}

このように、クラス間で共通の処理を親クラス(HttpServlet)にまとめて、異なる処理をそれぞれの子クラスのメソッド(doGet)に記述することで、ソースコードの重複がなくなりました。また、新しい機能(サーブレット)を追加する際には、親クラスを継承してメソッドを1つ実装するだけでよくなったので、誰もが簡単に実装できるようになりました。パターン適用前と比べて、見違えるほど美しくなりましたね。

Template MethodパターンのGoFでの定義は「ある操作におけるアルゴリズムの骨格を定義し、いくつかの処理の定義についてはサブクラスに任せる。そして、アルゴリズムの構造を変更することなく、その中に含まれる処理の再定義を行う1」です。ここでの「アルゴリズムの構造」は、親クラスで行っている、HTTPメソッドで処理を分けて、最終更新日時のヘッダによっても処理を分けて、HTMLやステータスコードを返すというロジックに相当します。また、「処理の再定義」は子クラスで行っている、HTMLを返す部分に相当します。

Template Methodパターンへの専門家のコメント

多くの専門家からも、Template Methodパターンを評価するコメントが寄せられています。

lang_and_engineさん

Java言語ではabstractなクラスは何のために作れるようになってるのか?という点から学べば,何の抵抗もなくこのパターンを受け入れられる。

GoFの23のデザインパターンを,Javaで活用するための一覧表 より

結城浩さん

スーパークラスのテンプレートメソッドでアルゴリズムが記述されていますので、サブクラス側ではアルゴリズムをいちいち記述する必要がなくなります。

『Java言語で学ぶデザインパターン入門』 より

最後に

わざわざ美術館に行かなくても、たった数行のコードを眺めるだけで知的な愉しみを味わうことができるのは、プログラマーの醍醐味でしょう。

Template Methodパターンの芸術性に共感してくださったエンジニアの方は、ぜひ当社(クオリサイトテクノロジーズ株式会社)の採用担当までご連絡ください!

関連記事

インスタンスを作る

インターフェイスをシンプルにする

他のクラスに任せる

参考URL

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした