ちゃんなるぶろぐ

エンジニア5年生🧑‍💻 オライリーとにらめっこする毎日。

【10分でわかる🥫】TypeScriptでDesign Pattern〜State Pattern〜

どうも、ちゃんなるです!

今回は、State Patternを紹介します🖐️

概要

State Patternは、オブジェクト指向プログラミングのデザインパターンのひとつで、オブジェクトの内部状態が変化することで、その振る舞いを変更させることができます。

これにより、コードの可読性やメンテナンス性が向上します。

今回示すプログラムの設計

具体的なシナリオとして、自動販売機の動作を模したプログラムを考えます。

自動販売機は、商品の購入やコイン投入など、さまざまな状態を持つことがあります。

State Patternを使用して、これらの状態遷移をスムーズに実装しましょう。

ざっくり仕様

  • 初期状態は「コイン未投入状態」で、コインが投入されると「コイン投入済状態」に移行します。

  • 商品が選択されると、「商品発売状態」に移行して商品が販売されます。また、商品が販売された後は、再度「コイン未投入状態」に戻ります。

  • 在庫が切れると、「在庫切れ状態」に移行して、コインが投入されても商品が選択できず、また返金もできなくなります。

クラス図

サンプルコードのクラス図:Mermaid Live Editorで作成

クラス名 役割
VendingMachine 自動販売機の動作を定義するクラス
State 状態のインターフェースを定義するクラス
HasCoinState コインが投入された状態を表すクラス
NoCoinState コインが投入されていない状態を表すクラス
SoldState 商品が販売された状態を表すクラス
SoldOutState 商品が売り切れた状態を表すクラス

サンプルコード

interface State {
  insertCoin(): void;
  selectItem(): void;
  returnCoin(): void;
}
class VendingMachine {
  state: State;
  // 各状態のインスタンスを生成
  hasCoinState = new HasCoinState(this);
  noCoinState = new NoCoinState(this);
  soldState = new SoldState(this);
  soldOutState = new SoldOutState(this);

  constructor() {
    this.state = this.noCoinState; // 初期状態はNoCoinState
  }

  insertCoin() {
    this.state.insertCoin();
  }

  selectItem() {
    this.state.selectItem();
  }

  returnCoin() {
    this.state.returnCoin();
  }

  setState(state: State) {
    this.state = state;
  }
}
class HasCoinState implements State {
  vendingMachine: VendingMachine;

  constructor(vendingMachine: VendingMachine) {
    this.vendingMachine = vendingMachine;
  }

  insertCoin() {
    console.log("既にコインが投入されています。");
  }

  selectItem() {
    console.log("商品が選択され、商品が出荷されます。");
    this.vendingMachine.setState(this.vendingMachine.soldState);
  }

  returnCoin() {
    console.log("コインが返却されます。");
    this.vendingMachine.setState(this.vendingMachine.noCoinState);
  }
}
class NoCoinState implements State {
  vendingMachine: VendingMachine;

  constructor(vendingMachine: VendingMachine) {
    this.vendingMachine = vendingMachine;
  }

  insertCoin() {
    console.log("コインが投入されました。");
    this.vendingMachine.setState(this.vendingMachine.hasCoinState);
  }

  selectItem() {
    console.log("コインを投入して下さい。");
  }

  returnCoin() {
    console.log("コインが投入されていません。");
  }
}
class SoldState implements State {
  vendingMachine: VendingMachine;

  constructor(vendingMachine: VendingMachine) {
    this.vendingMachine = vendingMachine;
  }

  insertCoin() {
    console.log("商品が出荷中のため、しばらくお待ちください。");
  }

  selectItem() {
    console.log("既に商品が出荷されています。");
  }

  returnCoin() {
    console.log("商品は既に選択されているため、コインは返却できません。");
  }
}
class SoldOutState implements State {
  vendingMachine: VendingMachine;

  constructor(vendingMachine: VendingMachine) {
    this.vendingMachine = vendingMachine;
  }

  insertCoin() {
    console.log("売り切れのため、コインを投入できません。");
  }

  selectItem() {
    console.log("売り切れです。");
  }

  returnCoin() {
    console.log("コインが投入されていません。");
  }
}

それでは、実際に使ってみましょう🖐️

この例では、VendingMachine クラスのインスタンスを作成し、続いてコインを投入し、商品を選択しています。

その後、再度コインを投入しようとしますが、既に商品が選択されているため、挿入できません。

最後に、コインを返却しようとしますが、商品が選択されているため、返却できません。

const vendingMachine = new VendingMachine();

vendingMachine.insertCoin(); // コインが投入されました。
vendingMachine.selectItem(); // 商品が選択され、商品が出荷されます。
vendingMachine.insertCoin(); // 商品が発売中のため、しばらくお待ちください。
vendingMachine.returnCoin(); // 商品は既に選択されているため、コインは返却できません。

State Patternの使い道

  • オブジェクトの状態に応じて振る舞いを変更する必要がある場合
  • 状態遷移のルールが複雑で、コードの可読性を向上させたい場合
  • 新しい状態を追加することが容易になるよう、状態遷移のコードを分離・整理したい場合

組み合わせられるデザインパターン

chan-naru.hatenablog.com

  • Singleton Pattern: 一つの状態オブジェクトをシステム全体で共有する場合

chan-naru.hatenablog.com

  • Command Pattern: 状態遷移に対応するコマンドを実行することで、履歴管理やアンドゥ・リドゥ機能を実装する場合

※執筆中

まとめ

この記事では、State Patternの基本概念と利点について解説しました。

自動販売機の動作を模したプログラムをTypeScriptで実装することで、状態遷移をスムーズに実現する方法を示しました。

また、State Patternが適用される典型的なシナリオや、他のデザインパターンとの組み合わせも紹介しました。

State Patternを活用することで、オブジェクトの状態に応じた振る舞いを分かりやすく表現し、コードの可読性やメンテナンス性を向上させることができます。

状態遷移が複雑なプログラムや、将来的に状態の追加が想定されるプロジェクトにおいて、ぜひState Patternの導入を検討してみてください。

参考文献

www.oreilly.com

en.wikipedia.org