どうも、ちゃんなるです!
今回は、State Patternを紹介します🖐️
概要
State Patternは、オブジェクト指向プログラミングのデザインパターンのひとつで、オブジェクトの内部状態が変化することで、その振る舞いを変更させることができます。
これにより、コードの可読性やメンテナンス性が向上します。
今回示すプログラムの設計
具体的なシナリオとして、自動販売機の動作を模したプログラムを考えます。
自動販売機は、商品の購入やコイン投入など、さまざまな状態を持つことがあります。
State Patternを使用して、これらの状態遷移をスムーズに実装しましょう。
ざっくり仕様
初期状態は「コイン未投入状態」で、コインが投入されると「コイン投入済状態」に移行します。
商品が選択されると、「商品発売状態」に移行して商品が販売されます。また、商品が販売された後は、再度「コイン未投入状態」に戻ります。
在庫が切れると、「在庫切れ状態」に移行して、コインが投入されても商品が選択できず、また返金もできなくなります。
クラス図
クラス名 | 役割 |
---|---|
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の使い道
- オブジェクトの状態に応じて振る舞いを変更する必要がある場合
- 状態遷移のルールが複雑で、コードの可読性を向上させたい場合
- 新しい状態を追加することが容易になるよう、状態遷移のコードを分離・整理したい場合
組み合わせられるデザインパターン
Strategy Pattern
: 状態に応じてアルゴリズムを選択することができる
Singleton Pattern
: 一つの状態オブジェクトをシステム全体で共有する場合
Command Pattern
: 状態遷移に対応するコマンドを実行することで、履歴管理やアンドゥ・リドゥ機能を実装する場合
※執筆中
まとめ
この記事では、State Patternの基本概念と利点について解説しました。
自動販売機の動作を模したプログラムをTypeScriptで実装することで、状態遷移をスムーズに実現する方法を示しました。
また、State Patternが適用される典型的なシナリオや、他のデザインパターンとの組み合わせも紹介しました。
State Patternを活用することで、オブジェクトの状態に応じた振る舞いを分かりやすく表現し、コードの可読性やメンテナンス性を向上させることができます。
状態遷移が複雑なプログラムや、将来的に状態の追加が想定されるプロジェクトにおいて、ぜひState Patternの導入を検討してみてください。