LESSON 25分

ストーリー

高橋アーキテクト
設計図エディタで図形をコピー&ペーストするとき、何が起きていると思う?
あなた
新しいオブジェクトを作って、元の値をコピー…ですか?
高橋アーキテクト
そうだ。でも、コピー元のオブジェクトが複雑な内部状態を持っていたら? 生成に重い初期化処理が必要だったら? そこでPrototypeパターンの出番だ

Prototype パターンとは

目的

既存のオブジェクト(プロトタイプ)をコピー(クローン)して新しいオブジェクトを生成する。コンストラクタを介さずに、実行時の状態をそのまま複製できる。

どんなときに使うか

状況説明
生成コストが高いDB読み込みやAPI呼び出しが必要なオブジェクト
設定が多い多数のプロパティが設定済みのテンプレート
実行時に型が決まるコンパイル時にどのクラスかわからない
状態の保存・復元スナップショットを取りたい

基本実装

// Clone可能なオブジェクトのインターフェース
interface Cloneable<T> {
  clone(): T;
}

// 図形の基底クラス
abstract class Shape implements Cloneable<Shape> {
  constructor(
    public x: number,
    public y: number,
    public color: string
  ) {}

  abstract clone(): Shape;
  abstract draw(): string;
}

// 具体クラス:円
class Circle extends Shape {
  constructor(
    x: number,
    y: number,
    color: string,
    public radius: number
  ) {
    super(x, y, color);
  }

  clone(): Circle {
    return new Circle(this.x, this.y, this.color, this.radius);
  }

  draw(): string {
    return `Circle(${this.x}, ${this.y}, r=${this.radius}, ${this.color})`;
  }
}

// 具体クラス:長方形
class Rectangle extends Shape {
  constructor(
    x: number,
    y: number,
    color: string,
    public width: number,
    public height: number
  ) {
    super(x, y, color);
  }

  clone(): Rectangle {
    return new Rectangle(this.x, this.y, this.color, this.width, this.height);
  }

  draw(): string {
    return `Rect(${this.x}, ${this.y}, ${this.width}x${this.height}, ${this.color})`;
  }
}

// 使い方
const original = new Circle(10, 20, 'red', 50);
const copy = original.clone();
copy.x = 100; // コピーを変更しても元に影響しない
copy.color = 'blue';

console.log(original.draw()); // Circle(10, 20, r=50, red)
console.log(copy.draw());     // Circle(100, 20, r=50, blue)

深いコピー(Deep Clone)

ネストされたオブジェクトを持つ場合、浅いコピーでは参照が共有されてしまいます。

// 浅いコピーの問題
class Document implements Cloneable<Document> {
  constructor(
    public title: string,
    public sections: Section[]
  ) {}

  // 浅いコピー -- sections配列の参照が共有される!
  shallowClone(): Document {
    return new Document(this.title, this.sections);
  }

  // 深いコピー -- sections も新しい配列・オブジェクトとしてコピー
  clone(): Document {
    const clonedSections = this.sections.map(section => section.clone());
    return new Document(this.title, clonedSections);
  }
}

class Section implements Cloneable<Section> {
  constructor(
    public heading: string,
    public content: string
  ) {}

  clone(): Section {
    return new Section(this.heading, this.content);
  }
}

// 深いコピーの確認
const doc = new Document('Report', [
  new Section('Introduction', 'Hello'),
  new Section('Body', 'Content'),
]);

const copy = doc.clone();
copy.sections[0].heading = 'Overview'; // コピーだけ変更

console.log(doc.sections[0].heading);  // 'Introduction'(元は変わらない)
console.log(copy.sections[0].heading); // 'Overview'

プロトタイプレジストリ

よく使うプロトタイプをレジストリに登録しておき、名前で取得する方法です。

class ShapeRegistry {
  private prototypes: Map<string, Shape> = new Map();

  register(name: string, shape: Shape): void {
    this.prototypes.set(name, shape);
  }

  get(name: string): Shape {
    const prototype = this.prototypes.get(name);
    if (!prototype) throw new Error(`Unknown shape: ${name}`);
    return prototype.clone();
  }
}

// レジストリにテンプレートを登録
const registry = new ShapeRegistry();
registry.register('small-red-circle', new Circle(0, 0, 'red', 10));
registry.register('large-blue-rect', new Rectangle(0, 0, 'blue', 200, 100));
registry.register('icon-circle', new Circle(0, 0, 'gray', 16));

// 名前で取得してカスタマイズ
const shape1 = registry.get('small-red-circle');
shape1.x = 50;
shape1.y = 100;

const shape2 = registry.get('large-blue-rect');
shape2.x = 200;

TypeScript / JavaScript での活用

TypeScript では structuredClone やスプレッド構文でも簡易的なクローンが可能です。

// スプレッド構文(浅いコピー)
const config = { host: 'localhost', port: 3000, options: { debug: true } };
const copy1 = { ...config };

// structuredClone(深いコピー)-- 関数やクラスインスタンスは非対応
const copy2 = structuredClone(config);

// Prototypeパターンが必要な場面
// - クラスのメソッドも含めてコピーしたい
// - clone時にカスタムロジックが必要
// - ポリモーフィズムを活かしたい(Shape の具体型を知らずにクローン)

高橋アーキテクトのアドバイス:

「TypeScript では、単純なデータオブジェクトなら structuredClone で十分なことも多い。Prototype パターンが真価を発揮するのは、クラスの振る舞いも含めて複製したい場合や、ポリモーフィックにクローンしたい場合だ」


まとめ

ポイント内容
Prototype の目的既存オブジェクトをクローンして新しいオブジェクトを生成
深いコピーネストされたオブジェクトも独立してコピー
レジストリテンプレートを登録して名前でクローンを取得
TypeScript活用単純なケースでは structuredClone も検討

チェックリスト

  • Prototype パターンの目的と使い所を理解した
  • 浅いコピーと深いコピーの違いを説明できる
  • プロトタイプレジストリを実装できる

次のステップへ

次は演習です。ここまで学んだ生成パターン(Factory Method、Abstract Factory、Builder、Singleton、Prototype)を実際に実装してみましょう。


推定読了時間: 25分