よしたろうブログ

駆動設計・アーキテクチャ・変更容易性とかの話が好きです。

「初心者のためのデザインパターン入門」シリーズ 第2回【Template Method - 順序と詳細の分離 - 】

前置き

引用元:Java言語で学ぶデザインパターン入門第3版

デザインパターンはクラスの再利用化を促進するものです。 再利用化を促進するとは、 クラスを部品として使えるようにするということであり、1つの部品を修正しても、他の部品の修正が少なくてすむ、ということなのです。

本記事は当ブログ管理人よしたろうによる『GoFデザインパターン入門シリーズ』です。本記事では、各デザインパターンの総合的理解ができることをコンセプトにしております。ピンポイントな情報ではなく、理解に必要な周辺知識をできる限り記述していきます。一記事あたりの文章量は多くなる傾向になります。あまりそういった記事が見当たらないこと、自身の学習のためにそういった形式にしております。ただし、他の記事で記述したものに関しては引用や紹介で済ませます。

デザインパターンって何?という方は過去記事

「Java言語で学ぶデザインパターン入門」シリーズ 第0回 【デザインパターンとは?】

をご覧下さい。

今回の記事の 『Template Method』 は「抽象クラス」と「継承」が登場します。前回の 『第一回 Iteratorパターン』 と比べると抽象度は低く、具象度の高いパターンだと思います。

適用箇所として

  • 『処理順序が決まっているけど、特定のケースごとに詳細がひとつひとつ異なる状況』

という、プログラミングにおいて頻発する場面で用いられるからです。またこのパターンは非常にシンプルな構成になっており、当たり前というか自然な発想を実現したものです。なので、デザインパターンという程のものではないかもしれません。ですが、これ単体でも十分学習する価値は高いと思います。

  1. プログラミングのセンスを磨ける
  2. 正しい継承・差分プログラミングを学べる
  3. 抽象クラス・抽象メソッドの意義を理解できる
  4. 具体的なクラスだけでのプログラムではなぜいけないのか

そういったことの気付きになると思うからです。まだ一年目の私にとっては非常に有用でした。逆に、歴の長い方からすると「なにを今更...」の様な感想になるかもしれません。

1. Template Method パターンのGoF本におけるカテゴライズ

オブジェクトの生成に関するパターン 使い所
Template Method
(具体的な処理をサブクラスに任せる)
1つのオペレーションにアルゴリズムのスケルトンを定義しておき、その中のいくつかのステップについては、サブクラスでの定義に任せる事にする。Template Methodパターンでは、アルゴリズムの構造を変えずに、アルゴリズム中のあるステップをサブクラスで定義する。 一連の処理の流れが決まっているが、様々なパターンがある時。
目的
生成 構造 振る舞い
範囲 クラス Factory MethodAdapter Interpreter / Template
オブジェクト Abstract Factory / Builder / Prototype / Singleton Adapter / Bridge / Composite / Decorator / Façade / Flyweight / Proxy Chain of Responsibility / Command / Iterator / Mediator / Memento / Observer / State / Strategy / Visitor

1-2. 2009年に再定義されたGoFデザインパターンでのカテゴライズ

Core(重要) Creational(生成) Periphenal(あまり使用されない) Other(その他)
Composite / Strategy / State / Command / Iterator / Proxy / Template Method / Facade Factory / Prototype / Builder / Dependency Injection Abstract Factory / Visitor / Decorator / Mediator / Type Object / Null Object / Extension Object/Interface Flyweight / Interpreter

詳細は以下の過去記事に記述してますので気になればご覧ください

Template Method パターンのまえに、「継承」と「抽象クラス」について詳しく知りたい方は以下の過去記事をご覧ください。かなり長いですのでご了承下さいませ。

『継承・インターフェイス・抽象クラス』シリーズ

2. Template Methodパターンの概要

TemplateMethodパターンはアルゴリズム(処理順序)の不変な部分(骨格部分)をスーパークラスで定義し、変わりうる部分をサブクラスで実装するパターンです。

『処理順序』と『(実装の)詳細』の『分離』が重要なポイントです。それにより、具象クラスの方では処理順序を逐一記述する必要がなくなります。抽象クラスの段階で『処理の流れを形作る』のです。単純な差分プログラムとは大きく異なります。サブクラスにおいて順序を意識する必要がなくなります。

処理順序が決まっているけど、特定のケースごとに詳細がひとつひとつ異なる状況で使用します。if 文で分岐させるといった方法でそれを実現する場合、分岐が増えれば増えるほど、その都度 else if 文を追加しなければならなくなります。

そういった部分を抽象メソッドに置き換えて、サブクラスに実装させポリモーフィズムにするのが Template Methodパターンです。継承を用いてサブクラスに実装を強制しています。

継承の功罪の話は多く、「デメリットが上回る事が多い」が一般的な結論かと思います。Rust や Golang では継承無くなってたりしますし。Template Method パターンでは抽象クラスを継承(拡張)していますが、以下の点で有用な継承なのかなと思っています。

  • 継承が深くならず基本的に二階層(順序がスーパー、詳細はサブなのでそれ以上は基本的にいらないはず)。
  • 期待される役割・振る舞いが同一なスーパークラスを利用したいための継承(行いたい処理自体は同じ)。
    • 継承関係になるクラス間で正しくis-a関係・リスコフの置換原則が成り立っている。

ただ、Template Method パターンは継承を用いるという点で間違った継承がおこなわれない様にはチーム内での共通認識や拡張方法の統一などの周知が必要なのが面倒臭い様な気がします。

追記:2022/10/16 また、OCP:開放経鎖原則を守るために適用されるパターンでもあります。 OCPとは一言で言ってしまえば、クライアントが具体的な処理が記述されているモジュールを呼び出す前にインターフェイスを挟むことで詳細から抽象に依存を逆転し、仕様変更などの拡張の際にはインターフェイスの具象クラスとしてサブクラスを作成することで、既存のコードを変更せずに拡張を実現する原則の事(全く一言ではない)。

変更しなくてはいけないのは以下の二点

  1. 新しいモジュールの追加
  2. そのオブジェクトをインスタンス化する main 関連

簡単に説明している図解を過去記事に書いているので参考にどうぞ

項:インターフェイスで可変を抽出する

「閉じる」には抽象を用いなければなりません。Javaでいえば、「抽象クラス」と「インターフェイス」です。今回の Template Methodパターンでは抽象クラスを用いてOCPを実現しています。

3. サンプルプログラム

参考・引用元:Java言語で学ぶデザインパターン入門第3版

名前 解説
AbstractDisplay open / print / close の抽象メソッドが定義
display メソッドが実装されている抽象クラス
CharDisplay open / print / close が実装されている具象クラス
StringDisplay open / print / close / printLine が
実装されている具象クラス
Main 動作確認用クラス

3-1. サンプルプログラムのクラス図

3-2. サンプルプログラムのコード

AbstractDisplay.class

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

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

StringDisplay.class

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

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

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

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

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

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

CharDisplay.class

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

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

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

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

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

Main.class

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();
    }
}

出力結果

3-3 それぞれの動き

  1. 抽象クラス AbstractDisplay クラス

    • open / print / close は抽象メソッド
    • display メソッドのみが具体的に実装
    • final修飾子は上書き不可を表す
      • 外部からの display メソッドの変更防止
      • サブクラスに display メソッドのを変更せずに拡張しろと言ってる
    • 抽象メソッドによって実装をサブクラスに延期
    • メソッド可視性をprotected
      • 継承して使うなら見え、そうでないなら見えない
      • ただし、同一パッケージ内では可視なので不完全なカプセル化
        • パッケージ管理でうまいことする必要がある
  2. Main クラス

  3. 参照型変数 d1・d2 から display メソッドの呼び出し

    • 抽象(基底・スーパー)クラスで実装された display メソッドの実行
    • その中で定義された抽象メソッドの呼び出し
    • 具象化された抽象メソッドの実行

4 このパターンの特徴

Template Methodパターンは「継承」を利用しているパターンです。

4-1. 不変と可変の分離・実装の強制

スーパークラス側では、可変な部分を抽象メソッドとして定義し、不変な部分のみを実装します。また、抽象メソッドはサブクラスに実装を強制します。
抽象クラス(抽象メソッドを持つクラス)はサブクラスに抽象メソッドの実装を強制することにより、サブクラスに確実に責務を負わせます。
これを subclass responsibility(サブクラスの責任)と呼び、この性質によりサブクラスで必ず具体的な実装を行う必要があります。

4-2. ポリモーフィズム

重要な点として抽象クラスのサブクラスが共通の型も継承している為、抽象クラス型の変数にサブクラスのインスタンスを代入可能にしています。
抽象クラス型変数に代入されてるインスタンスからはサブクラスに共通の抽象メソッドを呼び出すことができるようになります。同じメソッドの名前でもこのそれぞれ違う動作になります。同じメソッドでも異なる振る舞いを実行できる性質はポリモーフィズムの一種です。

4-3. サブクラスとスーパークラスの同一視

また、すべてのサブクラスの型はスーパクラスの型と見ることができます。これは、後述するリスコフの置換原則(LSP:The Liskov Substituion Principle)で度々説明される「サブクラスの型はその親クラスの型と置換可能」という点において非常に重要な性質です。

以下の過去記事に他のプログラムでその点を解説しています。

3-4-2. 実用例 ~ Abstract Pattern ~

yoshitaro-yoyo.hatenablog.com

4-4. 制御の反転(Inversion of Control、IoC

親クラスがサブクラスのメソッドを呼び出す「制御の反転」という特長を持つ。手続き型プログラミングと比べると制御の方向が反転しているため、「制御の反転」と呼ばれます。

手続き型プログラミングにおいて、処理の順序はコードの中核部分で制御されています。Mainクラスが全て制御する形です。

Template Methodパターン によって制御の反転が起きると、呼び出し側(Mainクラス)は応答を得るが、いつどのようにして応答を得るかは呼び出し側が制御できないようになっています。逆に呼び出された側(AbstractDisplayクラス)がいつどのようにして応えるかを決定しています。Mainクラスが display メソッドを呼び出して以降の処理順序は呼び出された先で制御されています。

「制御の反転」は実装を分離するという利点を持ちますが、同時に全体として協調動作させるときに複雑さが増すという欠点も同時に発生させる。

実装の分離とは、if 文で実現していた個別の処理の違いをサブクラスでポリモーフィズムにしたことですね。複雑さを増す部分としては、スーパークラスとサブクラス間で密接な関係があるため、スーパークラスの実装をサブクラスが把握する必要があること・処理のパターンだけサブクラスが増加することがあてはまります。

参考:wiki 制御の反転

IoC を実現する方法として以下が紹介されています。

実装技法 オブジェクト指向プログラミングでは、制御の反転を実装するには幾つかの基本的な技法がある。それらを次に列挙する:

  • Factory パターンを使用する。
  • サービスロケータパターンを使用する。
  • 依存性の注入を使用する。たとえば、
    • コンストラクタ注入
    • パラメータ注入
    • セッター注入
    • インタフェース注入
  • テンプレートメソッドパターンを利用する
  • ストラテジーパターンを利用する
  • 文脈化された参照 (contextualized lookup)

マーティン・ファウラーの最初の論文では[8]、上記の3番目までを論じていた。制御の反転の種類に関する記述の中で[9] では最後の技法も言及されている。通常、文脈化された参照はサービスロケータを使って実装される。

技法よりも「制御の反転」をどういう目的で使うのかが重要である。「制御の反転」はこれらの技法に限ったものではない。

DIってIoCのサブセットだったんですね....。DIと Template Method って兄弟やったんやっていうのが面白い発見でした。
IoCと依存性逆転とはまた異なる概念ということで中々理解が難しいところですね。

4-5.抽象クラスに由来するもの

以下の過去記事に詳細を解説しているのでそちらを参照していただければと思います。

5. 注意点

以下が見られる場合は、当初の設計が崩れていることを示唆しています。
再設計か別のクラスで実現する必要があるかと思います。

  1. オーバーライドするメソッドの数が多い
    • 適切な抽象化が行われていない?
  2. オーバーライドするメソッドで、super (基底クラスの同名メソッド)を呼んでいる
  3. オーバーライドするメソッドで、基底クラスの別のメソッドを呼んでいる
    • 上記プログラムでは実装された抽象メソッド内でサブクラス内のメソッドを呼び出してますが、あれはOK
  4. コンストラクタをオーバーライドしている(親クラスにメンバ変数がある)
  5. 派生クラスが、基底クラスのメンバ変数を参照している
  6. コード量・メソッド数が増加
  7. 階層が深くなる
  8. 非is - a や LIP 違反
  9. 抽象化された性質・共通点の使用ではなく、単なる機能の再利用

7・8 ・9に関しては過去記事にて解説していますので以下を参考にして下さい。

yoshitaro-yoyo.hatenablog.com

5-1. is - a と LIP(リスコフの置換原則)

is - a

抽象クラスでは、複数のオブジェクトで共通する概念を表現するコードを抽出し、それをスーパークラスの具象メソッドとして定義します。簡単にいえば共通部分を括り出して重複を削除しています。大事なのは、その括り出される対象のオブジェクト達の選び方です。

括り出される対象のオブジェクト達であるサブクラスと、括り出されたコードの抽出先であるスーパクラスとが「is - a」の関係であることです。つまり、括り出したコード部分はスーパークラス・サブクラスの根幹的な性質である必要がります。これはクラスの継承でも同様のことが言えます。

「サブクラス」は「スーパークラス」である
ex. スマートフォンはPCである

この場合だと PC が抽象クラスでスートフォンがサブクラスですね。この「is - a」の文章が成立しないのであれば抽象クラスとしてまとめるべきでもなければ、それを継承してもいけないのです。継承は機能の受け継ぎとして用いられるイメージがつよいかもしれません。その側面もあると思いますが、それのみで継承を行うのは間違いです。単純に機能の再利用がしたい場合はコンポジションとデリゲーションを使用すべきです。

リスコフの置換原則

リスコフの置換原則(The Liskov Substitution Princiddle (LSP))とは「サブクラスの型はその親クラスの型と置換可能」であり、もう少し簡単に言うと「サブクラスは基底クラスの代わりとして振舞えなければならない」という原則です。

厳密にいうと、

SがTの派生型であれば T型のオブジェクトが使われている箇所は全て S型のオブジェクトで置換可能である。

と表現します。

引用:はらへメモ リスコフの置換原則

リスコフの置換原則とは?

  • Liskov Substitution Principle
  • 派生型は基底型と置換可能でなければならない
    • 型の継承関係の正当性は本来の性質とは別
      • 例: Rectangle → Square
  • 設計者はユーザーの視点で 合理的な仮定 をしなければならない
    • 契約による設計 ( Design By Contract )
      • 事前条件と事後条件を取り決めておく
    • IS-A関係のあるオブジェクトは同等に振る舞うべき
      • 例: SquareはRectangleのように振る舞えない
        • Squareは縦横の長さに別々の値を持てない
  • リスコフの置換原則に違反しているか判断する経験則
    • 派生クラスで機能が退化している
      • 基底クラスでは実装されているのに、派生クラスでは何もしないメソッドなど
    • 派生クラスから例外がはかれる
      • 基底クラスのユーザーが期待しない例外を派生クラスのメソッドが投げるなど
  • リスコフの置換原則は[開放/閉鎖原則]を有効にする主要な役割を果たす原則の一つ

上記の

  • 基底クラスでは実装されているのに、派生クラスでは何もしないメソッドなど

こちらはたとえば、WriteRead という抽象クラスがあります。こちらは名前の通り、何かを書き込みし読み込みを行うことを表現しています。この WriteRead を継承したサブクラス Write クラスを作成します。このクラスは名前の通り、書き込みのみを行うクラスです。その場合、スーパークラスではできた読み込みがサブクラスでは出来なくなってしまいます。

これはスーパークラスと派生クラスの『置換不可』であり、サブクラスでの『機能の退化』を意味しているので、リスコフの置換原則違反となります。

5-2. その他の注意点

  1. 派生となる処理の数だけ、サブクラスができるので管理が複雑になりやすい
  2. スーパークラスとサブクラスが密接に関連しているので共通化されたアルゴリズムの順序を把握する必要がある
  3. 継承自体のリスク(以下の過去記事にて詳細解説してます。)

yoshitaro-yoyo.hatenablog.com

6. Strategy パターンとの類似点と違い

引用元:Template Methodパターン vs Strategyパターン

Template Methodパターンと、Strategyパターンはどちらも「基本構造の決まっているものを、何らかの方法で一部を拡張する」というシーンでよく用いられます。

この2つのパターンの使い分けは「設計上のある課題を継承で対応するか、コンポジションで対応するか」という文脈に大変近い事なので、継承とコンポジションの対比 を理解すると様々なシーンで応用がきくと思います。

上記の内容を受けると、Template MethodパターンはIs-a関係が成立しており拡張の柔軟性を限定して良いような「比較的単純なケース」では扱いやすいけれども、複雑化するような場合にはStrategyパターンを検討していく方が良いと考えられるでしょう。また、迷うような場合には「Stategyパターンにしておく方がより柔軟でもあり、堅牢になりやすそうだ」と考えると指針としては良いはずです。

別の考え方として、必要になればリファクタリングしていくようなイメージもありそうですね。Template MethodのコードをStrategyに書き直すのはそれほど苦労しないと思いますので(逆は困難であったり、場合によってはかなり煩雑になる可能性があります)、初期にコストをかけられない場合にはそのイメージを持って選択するのも一つの考え方ではあると思います。

6-1. 継承か、委譲か

引用元:吉田誠一のホームページ:Template Methodパターン

継承によるTemplate Methodパターンか、委譲によるStrategyパターンか、どちらが良いか。それは、クラスごとに異なる処理が、単独でも意味のあるものかどうか、で決めればよい。

単独でも意味のあるメソッドであれば、他のクラスからも呼び出せる、publicメソッドとすべきだ。即ち、子クラスごとの差異を表す抽象メソッドを、publicメソッドとして公開してもふさわしいものであれば、委譲を使った方が良い。

逆に、子クラスごとに異なる処理をするメソッドが、単独では意味を為さず、Template Methodから呼ばれるためだけに存在するような場合は、他のクラスからは隠蔽した方が良い。即ち、 Template Methodパターンの使用例として最適なケースは、子クラスごとの差異を表す抽象メソッドが、protectedメソッドであるような場合である。

この項は引用オンリーですね。勉強不足なので自分の言葉で語れませんでした.....。Strategy パターンの記事を書く頃には自分の言葉で語れる様にしたいところですね。