普段よく使うJavaライブラリにも、GoFのデザインパターンが隠されています。日々の作業が忙しく見逃しがちですが、たまにはじっくり一種の芸術ともいえる美しい設計を味わってみましょう。
今回の芸術
ソースファイル
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>");
}
}
}
実行結果
サーバーサイドで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
メソッドを実装しても問題なく動きます。
...
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とクラス名のマッピングを書きます。
<?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
クラスを追加します。
...
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)) {
...
...
<!-- 追加分 -->
<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
が動き、ブラウザには次のように表示されます。
ここで、2つのサーブレットHelloServlet
とByeServlet
を見比べてみましょう。処理の流れは、どちらも次のようになっています。
処理の流れ
- リクエストメソッド(GET/POSTなど)で処理を分ける
- 最終更新日時のヘッダが付与されていることを確認する
- 付与されていなければHTMLを返す
- 付与されていて、更新があればHTMLを返す
- 付与されていて、更新がなければステータスコードのみを返す
2つのクラスで異なる個所は「HTMLを返す」ところの中身だけで、他はまったく同じです。処理の流れは同じなのにソースコードが重複していて、コピペして使いまわされたような状態になっています。苦しい実装で、美しくありませんね。
Template Methodパターンを使った場合
ここでTemplate Methodパターンを適用してみましょう。
クラス間の共通の処理は親クラスで実装して、異なる処理だけ子クラスで実装します。今回の例では、「共通の処理」は最終更新日時のヘッダによってフローを分けること、「異なる処理」はHTMLを生成することです。
まずは親クラスです。
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など)に対応する処理を実装します。以下のようになります。
...
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>");
}
}
}
...
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なクラスは何のために作れるようになってるのか?という点から学べば,何の抵抗もなくこのパターンを受け入れられる。
結城浩さん
スーパークラスのテンプレートメソッドでアルゴリズムが記述されていますので、サブクラス側ではアルゴリズムをいちいち記述する必要がなくなります。
最後に
わざわざ美術館に行かなくても、たった数行のコードを眺めるだけで知的な愉しみを味わうことができるのは、プログラマーの醍醐味でしょう。
Template Methodパターンの芸術性に共感してくださったエンジニアの方は、ぜひ当社(クオリサイトテクノロジーズ株式会社)の採用担当までご連絡ください!
関連記事
インスタンスを作る
- よく使うJavaライブラリで味わうデザインパターン - Factoryパターン
- よく使うJavaライブラリで味わうデザインパターン - Builderパターン
- よく使うJavaライブラリで味わうデザインパターン - Abstract Factoryパターン
インターフェイスをシンプルにする
他のクラスに任せる
- よく使うJavaライブラリで味わうデザインパターン - Template Methodパターン
- よく使うJavaライブラリで味わうデザインパターン - Strategyパターン