State(ステート)は、オブジェクトの状態(State)によって、振る舞いを変えるデザインパターンです。
if や switch で状態分岐を書く代わりに、状態ごとに クラスを分けて管理するのが特徴的。
「状態の追加・変更に強い」設計ができるので、自動販売機の状態変化でのサンプルパターンで学習を進めましょう。
よく使われる場面
・テキストエディタの入力モード ・キャラクターのアニメーション状態(Idle / Run / Jump) ・ネットワーク接続状態(Connected / Disconnected)
サンプルコード
次のような自動販売機の状態遷移をイメージしてください。1. 待機中(Idle) 2. お金投入中(HasMoney) 3. 商品を出す(Dispensing)
Stateインターフェイス
class State {
insertCoin() {}
pressButton() {}
dispense() {}
}
各Stateの実装
class IdleState extends State {
constructor(vendingMachine) {
super();
this.vendingMachine = vendingMachine;
}
insertCoin() {
console.log("コインが投入されました。");
this.vendingMachine.setState(this.vendingMachine.hasMoneyState);
}
}
class HasMoneyState extends State {
constructor(vendingMachine) {
super();
this.vendingMachine = vendingMachine;
}
pressButton() {
console.log("ボタンが押されました。商品を出します。");
this.vendingMachine.setState(this.vendingMachine.dispensingState);
}
}
class DispensingState extends State {
constructor(vendingMachine) {
super();
this.vendingMachine = vendingMachine;
}
dispense() {
console.log("商品を出しました。次のお客様を待機します。");
this.vendingMachine.setState(this.vendingMachine.idleState);
}
}
コンテキスト(本体)
class VendingMachine {
constructor() {
this.idleState = new IdleState(this);
this.hasMoneyState = new HasMoneyState(this);
this.dispensingState = new DispensingState(this);
this.state = this.idleState;
}
setState(state) {
this.state = state;
}
insertCoin() { this.state.insertCoin(); }
pressButton() { this.state.pressButton(); }
dispense() { this.state.dispense(); }
}
実行
const machine = new VendingMachine();
machine.insertCoin(); // コインが投入されました。
machine.pressButton(); // ボタンが押されました。商品を出します。
machine.dispense(); // 商品を出しました。次のお客様を待機します。
役割
| 役割 | 説明 |
|---|---|
| State(状態) | 振る舞いを定義するインターフェイス(抽象クラス) |
| ConcreteState | 状態ごとの具体的な処理(Idle / HasMoney / Dispensing) |
| Context(文脈) | 現在の状態を保持し、状態に応じて処理を委譲するクラス |
メリット
・if / switch の分岐が消える(条件分岐地獄を防ぐ)。 ・新しい状態を追加しても他のコードを壊さない。 ・オブジェクトの状態変化が明示的になり、デバッグしやすい。
デメリット
・状態ごとにクラスが増える(クラス数が多くなりがち)。 ・シンプルなロジックではオーバーエンジニアリングに見える。
あとがき
処理をそれぞれclass化することで、段階的に依存関係を減らして、それぞれの拡張などの以後運用を楽にする印象です。 順番をさらにコントロールする、デザインパターンのファサードと組み合わせることで、よりしっかりとした設計が組み立てられそうですね。おまけコード
canvas描画つきサンプルを作ってみました。説明
3つの状態(Idle / Walk / Attack)をStateクラスで切り替え。 Canvas上のキャラクターが状態ごとに動きやアニメーションを変える。 ボタンを押すたびにhero.setState()で状態を変更。
sample.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>State Pattern Canvas Demo</title>
<style>
body { text-align: center; font-family: sans-serif; background: #fafafa; }
canvas { border: 1px solid #ccc; margin-top: 10px; background: white; }
button { margin: 5px; padding: 8px 16px; font-size: 14px; }
</style>
</head>
<body>
<h2>🎮 Stateパターン Canvasデモ</h2>
<p>ボタンでキャラクターの状態を切り替え</p>
<div>
<button id="idleBtn">Idle</button>
<button id="walkBtn">Walk</button>
<button id="attackBtn">Attack</button>
</div>
<canvas id="gameCanvas" width="400" height="200"></canvas>
<script>
// === State基底クラス ===
class State {
enter() {}
update(ctx, x, y) {}
}
// === 各状態のクラス ===
class IdleState extends State {
update(ctx, x, y) {
ctx.fillStyle = 'gray';
ctx.beginPath();
ctx.arc(x, y, 20, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'black';
ctx.fillText('Idle', x - 15, y + 40);
}
}
class WalkState extends State {
constructor() { super(); this.offset = 0; }
update(ctx, x, y) {
this.offset += 0.1;
const dx = Math.sin(this.offset) * 5;
ctx.fillStyle = 'blue';
ctx.fillRect(x + dx - 15, y - 15, 30, 30);
ctx.fillStyle = 'black';
ctx.fillText('Walk', x - 15, y + 40);
}
}
class AttackState extends State {
constructor() { super(); this.frame = 0; }
update(ctx, x, y) {
this.frame++;
const size = 20 + Math.sin(this.frame * 0.3) * 10;
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'black';
ctx.fillText('Attack', x - 20, y + 40);
}
}
// === コンテキスト ===
class Character {
constructor() {
this.x = 200;
this.y = 100;
this.state = new IdleState();
}
setState(state) {
this.state = state;
}
update(ctx) {
this.state.update(ctx, this.x, this.y);
}
}
// === 実行部 ===
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
ctx.font = '14px sans-serif';
const hero = new Character();
document.getElementById('idleBtn').onclick = () => hero.setState(new IdleState());
document.getElementById('walkBtn').onclick = () => hero.setState(new WalkState());
document.getElementById('attackBtn').onclick = () => hero.setState(new AttackState());
function loop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
hero.update(ctx);
requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>
デモ
ボタンでキャラクターの状態を切り替え
0 件のコメント:
コメントを投稿