ウェブエンジニア珍道中

日々の技術的に関する経験を書いていきます、脱線もします。

typescriptでデザインパターンを書く -Template Method-

今回はtypescriptでTemplate Methodパターンを書いてみようと思います。(3種類目)

github.com

Template Methodパターンとは

親クラスで処理の概要を決めて、具体的な処理は小クラスで書いていくデザインパターンのことを言います。

例えば親クラスが「楽器」で、子クラスが「ギター」や「ピアノ」だとした場合、親クラスで「鳴らす」ことを定義して実際の鳴らし方は各子クラスで決めます。

ここで言う親クラスがひな形(テンプレート)となり、子クラスがひな形に沿って実際の処理を行います。

親クラスは直接インスタンスを作ることがないので抽象クラス(AbstractClass)と言い、逆に作ることがある子クラスは具象クラス(ConcreteClass)と言います。

サンプル

概要

文字や文字列を5回繰り返して表示するというプログラムです。

登場人物は以下の通りです。

  • AbstractDisplay

    • 抽象クラス
    • テンプレート
    • display()のみ実装
      • 他の抽象メソッドを使って文字を表示する
        • open() -> print() * 5回 -> close()を行う
      • 抽象メソッドを使っているこのメソッドをテンプレートメソッドと呼ぶ
  • CharDisplay

    • AbstractDisplayクラスを継承
    • 抽象メソッドを全て実装
    • 入力した文字(例:A)を<<AAAAA>>という形で表示を行う
  • StringDisplay

    • AbstractDisplayクラスを継承
    • 抽象メソッドを全て実装
    • 入力した文字列*5列を四角で囲んだ表示を行う
  • Main

    • 実際に動かすところ

プログラム

AbstractDisplay

テンプレートです。display()以外のopen() print()close()全て抽象メソッドです。

このクラスを継承したクラスが抽象メソッドを全て実装しなかった場合はエラーとなります。

export abstract class AbstractDisplay {
    abstract open(): void;
    abstract print(): void;
    abstract close(): void;

    public display() {
        this.open();
        for(var _i = 0; _i < 5; _i++) {
            this.print();
        }
        this.close();
    }
}

CharDisplay

一文字を受け取って5回表示するクラスです。typescriptにはcharがなかったのでstringで代用しています。

二文字以上入れるとエラーを吐くようにすることで似たような挙動にしました。

import { AbstractDisplay } from './abstractDisplay';

export class CharDisplay extends AbstractDisplay {
    // typescriptにchar型はないのでstringで代用(1文字でない場合はエラーを起こす)
    private ch: string;

    constructor(ch: string) {
        super();
        if(ch.length != 1) {
            throw("CharDisplay: length is invalid.");
        }
        this.ch = ch;
    }

    public open(): void {
        process.stdout.write("<<");
    }

    public close(): void {
        console.log(">>");
    }

    public print(): void {
        process.stdout.write(this.ch);
    }
}

SrtingDisplay

入力文字*5を四角で囲んで出力します。中身が少し違いますが、CharDisplayと容量は同じです。

import { AbstractDisplay } from './abstractDisplay';

export class StringDisplay extends AbstractDisplay {
    private st: string;

    constructor(string: string) {
        super();
        this.st = string;
    }

    public open(): void {
        this.printLine();
    }
    public close(): void {
        this.printLine();
    }
    public print(): void {
        console.log("|" + this.st + "|");
    }
    private printLine(): void {
        console.log("+" + Array(this.st.length + 1).join("-") + "+");
    }
}

Main

実際に各クラスを使って出力しています。抽象クラスの型の変数に代入しているのがポイントです。

import { AbstractDisplay } from './abstractDisplay'
import { StringDisplay } from './stringDisplay'
import { CharDisplay } from './charDisplay'

var d1: AbstractDisplay = new CharDisplay("A");
var d2: AbstractDisplay = new StringDisplay("hogehoge");
var d3: AbstractDisplay = new StringDisplay("fugafuga!");

d1.display();
d2.display();
d3.display();

実行結果

<<AAAAA>>
+--------+
|hogehoge|
|hogehoge|
|hogehoge|
|hogehoge|
|hogehoge|
+--------+
+---------+
|fugafuga!|
|fugafuga!|
|fugafuga!|
|fugafuga!|
|fugafuga!|
+---------+

メリット

ロジックの共通化

継承を使う際の目的の一つだと思いますが、抽象クラスのテンプレートメソッドに処理を共通化できます。

仮にテンプレートメソッドにバグが見つかった場合、テンプレートメソッドのみを直せば良いので楽です。

親クラスと子クラスを同じ扱いにできる

実際に行われる処理は違いますが、使い方は親クラスを見ればわかります。(今回はdisplay()を使えば良いようになっている)

親クラスの変数に子クラスのインスタンスを入れることで、その変数が何クラスかを気にせずに使うことができます。

こういった様に親クラスの変数に子クラスのインスタンスを入れて正しく動作させるようにする原則はリスコフの置換原則(LSP)といい、継承における一般的な原則だそうです。詳しくはこのサイトに書いてありました。(wikipediaは専門用語だらけでわからなかった)

think-on-object.blogspot.jp

まとめ

抽象クラスを有効活用したデザインパターンだと思いました。抽象クラスって意味あるのかと思っていましたが、便利だ!とやっと思いましたw

役割分担をする時に皆(モブプロ?)でテンプレートを作っておいて、子クラス毎で分担すると楽に分担できるんじゃない?と思いました。為替のシステムなら「通貨クラス」を皆で作って「日本円」とか「米ドル」とかを各個人で作るという感じでしょうか。

デザインパターンは23種類あるらしく今3種類目なので3/23ですね。失踪しないか心配です。誰か応援して下さい。

では今回はこのへんで。ありがとうございました。