🕺

デザインパターン ~Prototypeパターン~

2022/01/09に公開

Prototypeとは?

GoF(Gang of Four)デザインパターンの一つであり、クラスからインスタンスを生成するのではなく、インスタンスから別のインスタンスを作り出す方法です。
例えば、Type Scriptでインスタンスを生成する場合、「new Instance」で生成すると思います。
このようにnewを使ってインスタンスを生成する場合、newの後に「クラス名」を指定しなければなりません。
しかし、以下のような場面では、クラス名を指定せずにインスタンスを生成したくなる場合があります。

  • 種類が多すぎてクラスにまとめられない場合
  • クラスからのインスタン生成が難しい場合
  • フレームワークと生成するインスタンスを分けたい場合

コードを書いてみよう

今回は、文字列を枠線で囲って表示したり、下線を引いて表示したりする以下のプログラムを書いていきます。

パッケージ 名前 どんなクラスか
framework Product 抽象メソッドuseとcreateCloneが定義されているインターフェイス
framework Manager createCloneを使ってインスタンスを複製するクラス
無名 MessageBox 文字列を枠線で囲って表示するクラス
無名 UnderlinePen 下線を引いて表示するクラス
無名 index 動作テスト用

今回はTypeScriptで書いてみます。
まずは、インスタンスを複製を行うためのインターフェースを作っていきます。

Product.ts

interface Product {
  use(s: string): void;
  createClone(): Product;
}

次に、Managerクラスを作成します。
ManagerクラスはProductインターフェースを利用してインスタンスの複製を行うクラスです。
ポイントとして、具体的なクラス名を書いていないことです。
あくまで、Productインターフェースを利用して、インスタンスの複製を行うためのクラスなので、具体的にどんなクラスが複製されるのかを、このクラスは知る必要はないからです。

import { Product } from './product';

class Manager {
  showcase: { [key: string]: Product } = {};
  register(name: string, prot: Product) {
    this.showcase[name] = prot;
  }
  create(protoName: string): Product {
    const p: Product = this.showcase[protoName]
    return p.createClone();
  }
}

次に、Productインターフェースを実装するMessageBoxクラスを作成します。
MessageBoxクラスは、文字列を枠線で囲って表示するクラスです。
※cloneパッケージを使用しています

import { Product } from './product';
import * as clone from 'clone';

class MessageBox implements Product {
  symbol: string;
  constructor(symbol: string) {
    this.symbol = symbol;
  }

  use(s: string): void {
    console.log(`this is a massage'.${this.symbol}. ${s}`);
  }

  createClone(): Product {
    return <Product>clone(this);
  }
}

次に、Productインターフェースを実装するUnderlinePenクラスを作成します。
UnderlinePenクラスは、文字列に下線を表示するクラスです。
※cloneパッケージを使用しています

import { Product } from './product';
import * as clone from 'clone';
class UnderlinePen implements Product {
  symbol: string;
  constructor(symbol: string) {
   this.symbol = symbol;
  }
  
  use(s: string): void {
   console.log(`this is underlinepen. ${this.symbol}. ${s}`);
  }
  
  createClone(): Product {
   return clone<Product>(this);
  }
}

最後に動作確認用のindex.tsを作成します。
まず、Managerクラスからインスタンスを生成します。
その後、MessageBoxとUnderlinePenのそれぞれのインスタンスを名前付きで登録します。

index.ts

import { Manager } from "./manager";
import { MessageBox } from './messageBox';
import { Product } from './product';
import { UnderlinePen } from './underlinePen';

// 準備
const manager = new Manager();
const pen = new UnderlinePen("-");
const box1 = new MessageBox("+");
const box2 = new MessageBox("*");

manager.register("pen1", pen);
manager.register("box1", box1);
manager.register("box2", box2);

// 生成
const p1 = manager.create("pen1");
p1.use("hello");
const b1 = manager.create("box1");
b1.use("hello");
const b2 = manager.create("box2");
b2.use("hello");

Prototypeの使いどころ

冒頭で書いた、場合を例に詳細に書いていきます。

  • 種類が多すぎてクラスにまとめられない場合
    サンプルプログラムでは"-"、"+"、"*"の3つの雛形を作りました。この程度の数なら、別々にクラスを作ってnewをすれば良さそうですが、その気になれば"#"や"^"などの他の種類も作ることができます。
    その数が多くなってしまうと、ソースコードの管理が難しくなってくるので、そんな時にPrototypeは有効なのかなと思います。

Prototypeのメリット

大きなポイントとしては、オブジェクト指向の考え方にもあるように「部品の再利用」ができることかなと思います。
もちろんクラス名を書くことが悪いわけではないと思いますが、大切なのは、今回で言えばTypeScriptではなくても、再利用可能になっていることなのかなと思います。

参考サイト

https://refactoring.guru/design-patterns/prototype

Discussion