デザインパターンのすゝめ ~Template Method~

はじめに

皆さん、こんにちは!PS-SLの織田です。2025年最後のブログ投稿になります。今回は、『Java言語で学ぶデザインパターン入門』の第3章を読んだ感想をまとめていきたいと思います。第2章ではAdapterパターンを扱いましたが、第3章ではTemplate Methodパターンについて書かれています。第2章のブログはコチラからご覧ください。それでは詳しく見ていきましょう!

Template Methodパターンとは

Template Methodパターンとは「処理の枠組み(テンプレート)を親クラスで定義し、具体的な処理内容はサブクラスに実装させる」パターンです。文字で説明しても何のこっちゃという感じなので、具体的なコードとともに説明していきます。

処理の枠組みを定義する:AbstractDisplay.java

public abstract class AbstractDisplay {
    // open, print, closeはサブクラスに実装をまかせる抽象メソッド
    public abstract void open();
    public abstract void print();
    public abstract void close();

    // displayはAbstractDisplayで実装してるメソッド
    public final void display() {
        open();
        for (int i = 0; i < 5; i++) {
            print();
        }
        close();
    }
}

このクラスはTemplate Methodパターンの核心部分です。全体のおおまかな流れをここで定義しています。一方で、細かな処理については記述されていません。ここで注目すべきはdisplay()メソッドです。このメソッドは次のような処理の流れを定義しています:

1. open()で開始処理を実行

2. print()を5回繰り返し実行

3. close()で終了処理を実行

重要なのは、display()メソッドがfinal宣言されている点です。これにより、サブクラスがこの処理の流れを変更できないようにしています。一方、open()、print()、close()は抽象メソッドとして宣言されており、具体的な処理内容はサブクラスに委ねています。

具体的な実装例1:CharDisplay.java

public class CharDisplay extends AbstractDisplay {
    private char ch; // 表示すべき文字

    // コンストラクタ
    public CharDisplay(char ch) {
        this.ch = ch;
    }

    @Override
    public void open() {
        // 開始文字列として"<<"を表示する
        System.out.print("<<");
    }

    @Override
    public void print() {
        // フィールドに保存しておいた文字を1回表示する
        System.out.print(ch);
    }

    @Override
    public void close() {
        // 終了文字列として">>"を表示する
        System.out.println(">>");
    }
}

AbstractDisplayクラスで記述しなかった細かい処理を記述していきます。CharDisplayクラスは、文字を括弧で囲んで表示する実装です。open()で<<を表示し、print()で指定された文字を表示し、close()で>>を表示します。

実行すると次のような出力になります:

<<HHHHH>>

具体的な実装例2:StringDisplay.java

public class StringDisplay extends AbstractDisplay {
    private String string;  // 表示すべき文字列
    private int width;      // 文字列の表示幅

    // コンストラクタ
    public StringDisplay(String string) {
        this.string = string;
        this.width = string.length();
    }

    @Override
    public void open() {
        printLine();
    }

    @Override
    public void print() {
        System.out.println("|" + string + "|");
    }

    @Override
    public void close() {
        printLine();
    }

    // openとcloseから呼び出されて"+----+"という文字列を表示するメソッド
    private void printLine() {
        System.out.print("+");
        for (int i = 0; i < width; i++) {
            System.out.print("-");
        }
        System.out.println("+");
    }
}

おまけでもう1個処理のバリエーションを増やします。StringDisplayクラスは、文字列を罫線で囲んで表示する実装です。printLine()という独自のヘルパーメソッドを使用して、開始と終了時に罫線を表示します。

実行すると次のような出力になります:

+-------------+

|Hello, world.|

|Hello, world.|

|Hello, world.|

|Hello, world.|

|Hello, world.|

+-------------+

利用例:Main.java

public class Main {
    public static void main(String[] args) {
        // 'H'を持ったCharDisplayのインスタンスを1個作る
        AbstractDisplay d1 = new CharDisplay('H');

        // "Hello, world."を持ったStringDisplayのインスタンスを1個作る
        AbstractDisplay d2 = new StringDisplay("Hello, world.");

        // d1,d2とも、すべて同じAbstractDisplayのサブクラスのインスタンスだから
        // 継承したdisplayメソッドを呼び出すことができる
        // 実際の動作は個々のクラスCharDisplayやStringDisplayで定まる
        d1.display();
        d2.display();
    }
}

このクラスはTemplate Methodパターンを利用するクライアント側の実装例です。重要なポイントは、AbstractDisplay型の変数を宣言し、CharDisplayやStringDisplayのインスタンスを代入している点です。

どちらのインスタンスも同じdisplay()メソッドを呼び出していますが、実際の動作(open()、print()、close()の処理内容)は各サブクラスの実装によって異なります。

Template Methodパターンで嬉しいこと

「わざわざ抽象クラスを作る必要があるの?」と思う人がいるかもしれません。CharDisplayやStringDisplayに必要な処理を書けば、たしかにAbstractDisplayは削ることができます。しかし、このパターンを使うことで受けられる恩恵があります。例えば、処理の流れを変更したくなったとします。

ケース1:表示回数を変更したい

Template Methodパターンあり:

public abstract class AbstractDisplay {
    // 略

    // 表示回数を3回に変更したい
    public final void display() {
        open();
        for (int i = 0; i < 3; i++) {  // この1行だけ変更すれば済む
            print();
        }
        close();
    }
}

Template Methodパターンなし:

public class CharDisplay {
    // 略
    public void display() {
        System.out.print("<<");
        for (int i = 0; i < 3; i++) {  // すべてのクラスで変更が必要
            System.out.print(ch);
        }
        System.out.println(">>");
    }
}
public class StringDisplay {
    // 略
    public void display() {
        printLine();
        for (int i = 0; i < 3; i++) {  // こっちのクラスでも変更が必要;;
            System.out.println("|" + string + "|");
        }
        printLine();
    }
}

Template Methodパターンあり:親クラスの1箇所を修正するだけで、すべてのサブクラスの動作が変わる

Template Methodパターンなし:すべてのクラスで同じ修正を繰り返す必要がある

ケース2:処理の前後にログを追加したい

また、もっと複雑な処理をする際にはより重宝します。例えば、処理の開始と終了時にログを出力したくなったとします。

Template Methodパターンあり:

public abstract class AbstractDisplay {
    // 略

    public final void display() {
        System.out.println("[LOG] 処理開始");  // ログ追加
        open();
        for (int i = 0; i < 5; i++) {
            print();
        }
        close();
        System.out.println("[LOG] 処理終了");  // ログ追加
    }
}

Template Methodパターンなし:

public class CharDisplay {
    public void display() {
        System.out.println("[LOG] 処理開始");  // すべてのクラスに追加が必要
        System.out.print("<<");
        for (int i = 0; i < 5; i++) {
            System.out.print(ch);
        }
        System.out.println(">>");
        System.out.println("[LOG] 処理終了");  // すべてのクラスに追加が必要
    }
}
public class StringDisplay {
    public void display() {
        System.out.println("[LOG] 処理開始");  // こっちのクラスにも追加が必要
        printLine();
        for (int i = 0; i < 5; i++) {
            System.out.println("|" + string + "|");
        }
        printLine();
        System.out.println("[LOG] 処理終了");  // こっちのクラスにも追加が必要
    }
}

Template Methodパターンありの場合:ケース1と同様に修正が必要最小限に留まっています。AbstractDisplayにログ出力の処理を追記するだけで済みました。

Template Methodパターンなしの場合:ケース1と同様に、処理の流れが各クラスに分散しているため、同じような修正をすべてのクラスで実施する必要があります。そのため、修正漏れやバグの混入リスクが高まってしまいます

ケース3:新しい表示方法を追加したい

さらに、新しい表示方法を追加したい場合を考えてみましょう。例えば、XMLタグで囲んで表示するクラスを追加したいとします。

Template Methodパターンあり:

public class XmlDisplay extends AbstractDisplay {
    private String content;
    private String tagName;

    public XmlDisplay(String content, String tagName) {
        this.content = content;
        this.tagName = tagName;
    }

    @Override
    public void open() {
        System.out.print("<" + tagName + ">");
    }

    @Override
    public void print() {
        System.out.print(content);
    }

    @Override
    public void close() {
        System.out.println("</" + tagName + ">");
    }
}

Template Methodパターンなし:

public class XmlDisplay {
    private String content;
    private String tagName;

    public XmlDisplay(String content, String tagName) {
        this.content = content;
        this.tagName = tagName;
    }

    public void display() {
        // 処理の流れを毎回ゼロから実装する必要がある
        // もし他のクラスと処理の流れを変えたい場合、それも可能になってしまう(一貫性が失われる)
        System.out.print("<" + tagName + ">");
        for (int i = 0; i < 5; i++) {
            System.out.print(content);
        }
        System.out.println("</" + tagName + ">");
    }
}

Template Methodパターンありの場合:新しいクラスを追加するだけで、既存のコードを全く変更することなく、新しい表示方法を実装できます。しかも、処理の流れ(open() → 5回print() → close())は自動的に継承されるため、一貫性が保たれます。

Template Methodパターンなしの場合:新しいクラスを追加するたびに処理の流れをゼロから実装する必要があり、一貫性を保つのが開発者の責任になってしまいます。

まとめ

Template Methodパターンの本質は、「処理の流れの統一」と「変更の容易さ」の実現にあります。処理の枠組みを親クラスで一元管理することで、共通の処理フローを保証しつつ、具体的な実装の詳細は各サブクラスに委ねることができます。

特に重要なのは、以下の3つの利点です:

Template Methodパターンの本質は、「処理の流れの統一」と「変更の容易さ」の実現にあります。処理の枠組みを親クラスで一元管理することで、共通の処理フローを保証しつつ、具体的な実装の詳細は各サブクラスに委ねることができます。

特に重要なのは、以下の3つの利点です:

1. 一箇所の変更で全体に影響:処理の流れを変更したい場合、親クラスの1箇所を修正するだけで、すべてのサブクラスの動作が変わる
2. 一貫性の保証:すべてのサブクラスが同じ処理の流れを持つことが保証される
3. 拡張の容易さ:新しいサブクラスを追加する際、処理の流れを意識する必要がない

実際の開発では、「処理の流れは共通だが、詳細な処理内容だけが異なる」という場面が発生するかと思います。そんな時、Template Methodパターンはコードの重複を避け、保守性を向上させ、将来の拡張性も確保するというメリットをもたらしてくれます。

それではこの辺りで2025年最後のブログを締めたいと思います。個人的に大変学びの多い一年でした。来年も引き続きデザインパターンをはじめ多くのことを学んでいきたいと思います。それでは、よいお年を!

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です