デニッキ

XRエンジニア🔰の備忘録・日記です.

Template Method パターン [Java]


GoFデザインパターンについて学んでます.

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

  • 作者:結城 浩
  • 発売日: 2004/06/19
  • メディア: 大型本

今回は「Template Method」パターンについて学びます.

Template Method Pattern

基底クラスで処理の枠組みを定めて,拡張クラスでその具体的な内容を定めるデザインパターン

基底クラスの法にテンプレートになるメソッドを定義します.
そのメソッドの定義の中では抽象メソッドが使われており,基底クラスのプログラムを
読むだけでは最終的にどういう処理をするのかはわからない.
抽象メソッドを実際に実装するのは拡張クラスで,実装後に具体的な処理が決定します.

サンプルプログラム

「文字や文字列を5回繰り返して表示する」サンプルプログラムを利用します.
上の動作をテンプレートメソッドとします.

UML

f:id:xrdnk:20200325204320p:plain

AbstractDisplayクラス

package TemplateMethod;

public abstract class AbstractDisplay {
    public abstract void open();

    public abstract void print();

    public abstract void close();

    public final void display() {
        open();
        for (int i = 0; i < 5; i++) {
            print();
        }
        close();
    }
}

open(), print(), close()は抽象メソッドで,テンプレートメソッドdisplay()のみが実装されてます.
final修飾子があることで,テンプレートメソッドdisplay()は外部からいじれなくします.

open(),print(),close()自体何をするのかは拡張クラスに委ねます.

CharDisplayクラス

package TemplateMethod;

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() {
        System.out.print(ch);
    }

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

CharDisplayクラスではopen(),print(),close()の動きはソースの通りになります.
最初に"<<"を表示し,コンストラクタで与えられた1文字を表示し,最後に">>"を表示します.

StringDisplayクラス

package TemplateMethod;

public class StringDisplay extends AbstractDisplay {

    private String string;
    private int width;

    public StringDisplay(String string) {
        this.string = string;
        this.width = string.getBytes().length;
    }

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

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

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

    private void printLine() {
        System.out.print("+");
        for (int i = 0; i < width; i++) {
            System.out.print("-");
        }
        System.out.println("+");
    }
}

StringDisplayクラスの場合,"+-----+"を表示した後に,
コンストラクタで与えられた文字列を"|"と"|"と挟んで表示して,
最後に再度"+-----+"で括るようにしております.

Mainクラスと結果

package TemplateMethod;

public class Main {
    public static void main(String[] args) {
        // AbstarctDisplay型の変数にCharDisplayのインスタンスを代入している
        AbstractDisplay d1 = new CharDisplay('H');
        // AbstarctDisplay型の変数にStringDisplayのインスタンスを代入している
        AbstractDisplay d2 = new StringDisplay("Hello World!");

        d1.display();
        d2.display();
    }
}

Mainを実行すると以下のような結果になります.

<<HHHHH>>
+------------+
|Hello World!|
|Hello World!|
|Hello World!|
|Hello World!|
|Hello World!|
+------------+

反芻タイム

ロジック共通化

TemplateMethodパターンを利用することの利点として,
基底クラスのテンプレートメソッドでアルゴリズムが記述されているため,
拡張クラス側ではアルゴリズムを逐一記述する必要がなくなる.

Liskov Substitution Principle (LSP)

SOLID原則3のLです.リスコフの置換原則.

基底クラス型の変数に,拡張クラスのインスタンスのどれを代入しても正しく動作するようにする

派生型は基底型で決めた規則を変更してはいけないということです.

基底クラス視点と拡張クラス視点

拡張クラスの立場になると,

  • 基底クラスで定義されているメソッドが拡張クラスで利用できる
  • 拡張クラスに少しメソッドを書くだけで新しい機能が追加できる
  • 拡張クラスでメソッドをオーバーライドすれば振る舞いを変更できる

となるが,基底クラスの立場で考えてみると,

  • 拡張クラスがそのメソッドを実装することを期待する
  • 拡張クラスに対して,そのメソッドの実装を要請する

となる.
拡張クラスには基底クラスで宣言されている抽象メソッドを実装する責任を持つ.
これをsubclass responsibility(サブクラスの責任)という.

終わりに

Template Methodパターンをインスタンス生成に応用した例がFactory Methodパターン.
以前記事で投稿した奴です.

xrdnk.hateblo.jp

また,java.io.InputStreamクラス自体,Template Methodパターンが使われている.
(IO系はTemplate Methodパターンの宝庫だったりする.)

Template Method パターンでは「継承」を利用してプログラムの動作を変更しましたが,
「委譲」を利用してプログラムの動作を変更するデザインパターンがあります.
Strategyパターンです.そのうち記事を上げます.