State
状態をクラスで表現する
概要
オブジェクトの状態を変化させることで、処理内容を変えられるようにする。
マンガでわかる State
マンガでわかる State #デザインパターン - Qiita
でざぱたんで覚える State
ちびキャラは「ステートたん」。暗黒街に生きる戦災孤児の少女で、相手ごとに“別の自分”を丸ごと演じ分けて生き延びてきた。相手(=状態)ごとの振る舞いを個別のクラスとして用意し、外からは同じ顔のまま切り替える——利用側は「今どの状態か」を気にせずポリモーフィックに扱える、というStateの構造がそのままキャラの生き方になっている。双子の兄がストラテジーたん(Strategy)。
出典: いしだけ『でざぱたん: ちびキャラで覚えるデザインパターン』(P.014〜)
登場人物
- State: 状態を表すインタフェース
- ConcreteState: 具体的な状態たち
- Context: 状況、前後関係
クラス図
このサイトの実装(警備員室の金庫、昼/夜の状態切替)での対応関係:
classDiagram
class State {
<<interface>>
+doClock(context, hour)
+doUse(context)
+doAlarm(context)
+doPhone(context)
}
class Context {
<<interface>>
+setClock(hour)
+changeState(state)
+callSecurityCenter(msg)
+recordLog(msg)
}
class DayState {
<<singleton>>
+getInstance() State
+doClock(context, hour)
+doUse(context)
+doAlarm(context)
+doPhone(context)
}
class NightState {
<<singleton>>
+getInstance() State
+doClock(context, hour)
+doUse(context)
+doAlarm(context)
+doPhone(context)
}
class Frame
class SafeFrame {
-state State
+setClock(hour)
+changeState(state)
+callSecurityCenter(msg)
+recordLog(msg)
+actionPerformed(e)
}
State <|.. DayState
State <|.. NightState
Context <|.. SafeFrame
Frame <|-- SafeFrame
SafeFrame o-- State : state
DayState ..> NightState : changeState
NightState ..> DayState : changeState
やり形
- Stateインタフェースをシングルトンにする
- ConcreteStateごとに「自分自身に関する状態」だけを実装する
メリット
分割統治できる
- ConcreteStateごとの実装を考えればいいので、あるStateでは他のStateのことをある程度考慮から外すことができる(全部ではない)。
関連
所感
これはいいね。 他の状態のことを意識しないのはとても良い。 状態遷移図と合わせて大変効力を発揮しそう
と思ったけど、ConcreteStateで別のStateに変更することを呼び出してたらあまり依存解消できてないのでは? →Mediatorパターンで解決できる
Java
public class DayState implements State {
private static DayState singleton = new DayState();
private DayState() { // コンストラクタはprivate
}
public static State getInstance() { // 唯一のインスタンスを得る
return singleton;
}
public void doClock(Context context, int hour) { // 時刻設定
if (hour < 9 || 17 <= hour) {
context.changeState(NightState.getInstance());
}
}
public void doUse(Context context) { // 金庫使用
context.recordLog("金庫使用(昼間)");
}
public void doAlarm(Context context) { // 非常ベル
context.callSecurityCenter("非常ベル(昼間)");
}
public void doPhone(Context context) { // 通常通話
context.callSecurityCenter("通常の通話(昼間)");
}
public String toString() { // 文字列表現
return "[昼間]";
}
}
public interface Context {
public abstract void setClock(int hour); // 時刻の設定
public abstract void changeState(State state); // 状態変化
public abstract void callSecurityCenter(String msg); // 警備センター警備員呼び出し
public abstract void recordLog(String msg); // 警備センター記録
}
public class NightState implements State {
private static NightState singleton = new NightState();
private NightState() { // コンストラクタはprivate
}
public static State getInstance() { // 唯一のインスタンスを得る
return singleton;
}
public void doClock(Context context, int hour) { // 時刻設定
if (9 <= hour && hour < 17) {
context.changeState(DayState.getInstance());
}
}
public void doUse(Context context) { // 金庫使用
context.callSecurityCenter("非常:夜間の金庫使用!");
}
public void doAlarm(Context context) { // 非常ベル
context.callSecurityCenter("非常ベル(夜間)");
}
public void doPhone(Context context) { // 通常通話
context.recordLog("夜間の通話録音");
}
public String toString() { // 文字列表現
return "[夜間]";
}
}
public class Main {
public static void main(String[] args) {
SafeFrame frame = new SafeFrame("State Sample");
while (true) {
for (int hour = 0; hour < 24; hour++) {
frame.setClock(hour); // 時刻の設定
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}
}
public interface State {
public abstract void doClock(Context context, int hour); // 時刻設定
public abstract void doUse(Context context); // 金庫使用
public abstract void doAlarm(Context context); // 非常ベル
public abstract void doPhone(Context context); // 通常通話
}
import java.awt.Frame;
import java.awt.Label;
import java.awt.Color;
import java.awt.Button;
import java.awt.TextField;
import java.awt.TextArea;
import java.awt.Panel;
import java.awt.BorderLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
public class SafeFrame extends Frame implements ActionListener, Context {
private TextField textClock = new TextField(60); // 現在時刻表示
private TextArea textScreen = new TextArea(10, 60); // 警備センター出力
private Button buttonUse = new Button("金庫使用"); // 金庫使用ボタン
private Button buttonAlarm = new Button("非常ベル"); // 非常ベルボタン
private Button buttonPhone = new Button("通常通話"); // 通常通話ボタン
private Button buttonExit = new Button("終了"); // 終了ボタン
private State state = DayState.getInstance(); // 現在の状態
// コンストラクタ
public SafeFrame(String title) {
super(title);
setBackground(Color.lightGray);
setLayout(new BorderLayout());
// textClockを配置
add(textClock, BorderLayout.NORTH);
textClock.setEditable(false);
// textScreenを配置
add(textScreen, BorderLayout.CENTER);
textScreen.setEditable(false);
// パネルにボタンを格納
Panel panel = new Panel();
panel.add(buttonUse);
panel.add(buttonAlarm);
panel.add(buttonPhone);
panel.add(buttonExit);
// そのパネルを配置
add(panel, BorderLayout.SOUTH);
// 表示
pack();
show();
// リスナーの設定
buttonUse.addActionListener(this);
buttonAlarm.addActionListener(this);
buttonPhone.addActionListener(this);
buttonExit.addActionListener(this);
}
// ボタンが押されたらここに来る
public void actionPerformed(ActionEvent e) {
System.out.println(e.toString());
if (e.getSource() == buttonUse) { // 金庫使用ボタン
state.doUse(this);
} else if (e.getSource() == buttonAlarm) { // 非常ベルボタン
state.doAlarm(this);
} else if (e.getSource() == buttonPhone) { // 通常通話ボタン
state.doPhone(this);
} else if (e.getSource() == buttonExit) { // 終了ボタン
System.exit(0);
} else {
System.out.println("?");
}
}
// 時刻の設定
public void setClock(int hour) {
String clockstring = "現在時刻は";
if (hour < 10) {
clockstring += "0" + hour + ":00";
} else {
clockstring += hour + ":00";
}
System.out.println(clockstring);
textClock.setText(clockstring);
state.doClock(this, hour);
}
// 状態変化
public void changeState(State state) {
System.out.println(this.state + "から" + state + "へ状態が変化しました。");
this.state = state;
}
// 警備センター警備員呼び出し
public void callSecurityCenter(String msg) {
textScreen.append("call! " + msg + "\n");
}
// 警備センター記録
public void recordLog(String msg) {
textScreen.append("record ... " + msg + "\n");
}
}
Go
Java版と同じ題材(警備員室の金庫、昼/夜でState切り替え)を移植。DayState/NightStateは「非公開型 + パッケージ変数の即時初期化 + 公開アクセサ関数」でシングルトンを表現(Singleton/go版の「別解」に相当)。GUI(SafeFrame)はSafeConsoleという標準出力ベースの代役に置き換え、ボタン押下はPressUse/PressAlarm/PressPhoneという明示的なメソッド呼び出しにした。main.goが0〜23時を1周し、いくつかの時刻でボタン操作を自動的に仕込んで昼/夜の挙動差を出力で見せる。
実行: go run ./GoF/patterns/State/go
// 実行方法は main.go を参照(go run ./GoF/patterns/State/go)。
package main
// State は「状態」を表すインタフェース(Java版 State.java 相当)。
// ConcreteState(DayState/NightState)がこれを実装する。
//
// Go の interface は暗黙実装(implements を書かない)。
type State interface {
DoClock(context Context, hour int) // 時刻設定
DoUse(context Context) // 金庫使用
DoAlarm(context Context) // 非常ベル
DoPhone(context Context) // 通常通話
}
// 実行方法は main.go を参照(go run ./GoF/patterns/State/go)。
package main
// dayState はConcreteStateの一つ(昼間)。
//
// 型を非公開にし、生成手段をDayState()だけに絞ることで、Java版の
// 「private コンストラクタ + private static singleton フィールド」に相当する
// 唯一性を守る(Singleton/go版のコメントにある「別解」=パッケージ変数の
// 即時初期化がJava版に最も近い形、をここでは主形として採用している)。
type dayState struct{}
var dayStateInstance State = &dayState{}
// DayState は唯一のインスタンスを返す。
func DayState() State {
return dayStateInstance
}
// DoClock は時刻設定。9時未満・17時以降なら夜間へ遷移する。
func (s *dayState) DoClock(context Context, hour int) {
if hour < 9 || 17 <= hour {
context.ChangeState(NightState())
}
}
// DoUse は金庫使用。昼間はただ記録するだけ。
func (s *dayState) DoUse(context Context) {
context.RecordLog("金庫使用(昼間)")
}
// DoAlarm は非常ベル。昼間は通常どおり警備センターへ連絡する。
func (s *dayState) DoAlarm(context Context) {
context.CallSecurityCenter("非常ベル(昼間)")
}
// DoPhone は通常通話。昼間は警備センターへの通常の通話として扱う。
func (s *dayState) DoPhone(context Context) {
context.CallSecurityCenter("通常の通話(昼間)")
}
// String は fmt.Stringer を満たす(Java版 toString() 相当)。
func (s *dayState) String() string {
return "[昼間]"
}
// 実行方法は main.go を参照(go run ./GoF/patterns/State/go)。
package main
// nightState はConcreteStateの一つ(夜間)。DayStateと同じ理由でシングルトンにする。
type nightState struct{}
var nightStateInstance State = &nightState{}
// NightState は唯一のインスタンスを返す。
func NightState() State {
return nightStateInstance
}
// DoClock は時刻設定。9時以上17時未満になったら昼間へ遷移する。
func (s *nightState) DoClock(context Context, hour int) {
if 9 <= hour && hour < 17 {
context.ChangeState(DayState())
}
}
// DoUse は金庫使用。夜間の金庫使用は非常事態として警備センターへ緊急連絡する。
func (s *nightState) DoUse(context Context) {
context.CallSecurityCenter("非常:夜間の金庫使用!")
}
// DoAlarm は非常ベル。夜間も警備センターへ連絡する。
func (s *nightState) DoAlarm(context Context) {
context.CallSecurityCenter("非常ベル(夜間)")
}
// DoPhone は通常通話。夜間は録音だけしておき、警備員は呼ばない。
func (s *nightState) DoPhone(context Context) {
context.RecordLog("夜間の通話録音")
}
// String は fmt.Stringer を満たす(Java版 toString() 相当)。
func (s *nightState) String() string {
return "[夜間]"
}
// 実行方法は main.go を参照(go run ./GoF/patterns/State/go)。
package main
// Context はJava版 Context.java(インタフェース)相当。
// State側からコールバックされる「状況」に対する操作をまとめる。
type Context interface {
SetClock(hour int) // 時刻の設定
ChangeState(state State) // 状態変化
CallSecurityCenter(msg string) // 警備センター警備員呼び出し
RecordLog(msg string) // 警備センター記録
}
// 実行方法は main.go を参照(go run ./GoF/patterns/State/go)。
package main
import "fmt"
// SafeConsole はContextのConcreteState利用者(Java版 SafeFrame.java 相当)。
//
// 【GUIの置き換えについて】
// Java版のSafeFrameはAWTのFrame/Button/TextField/TextAreaでできたGUIで、
// ユーザーが「金庫使用」「非常ベル」「通常通話」「終了」ボタンを押すと
// actionPerformed(e)経由でState.doXxx(this)が呼ばれ、textClock/textScreenに
// 結果が描画される、という作りだった。
//
// このGo版にGUIは無いので、
// - textClock/textScreenへの描画 → fmt.Println (標準出力)
// - ボタン押下(金庫使用/非常ベル/通常通話) → PressUse/PressAlarm/PressPhone という
// 公開メソッド。main.go側がタイマー代わりのforループの中で、
// 「このボタンを押す」を明示的に呼び出す
//
// に置き換えている(終了ボタンはループが自然に終わるだけなので用意していない)。
type SafeConsole struct {
state State
}
// NewSafeConsole はコンストラクタ相当。Java版同様、初期状態は昼間から始まる。
func NewSafeConsole() *SafeConsole {
return &SafeConsole{state: DayState()}
}
// SetClock は時刻の設定。
func (c *SafeConsole) SetClock(hour int) {
fmt.Printf("現在時刻は%02d:00\n", hour)
c.state.DoClock(c, hour)
}
// ChangeState は状態変化。
func (c *SafeConsole) ChangeState(state State) {
fmt.Printf("%sから%sへ状態が変化しました。\n", c.state, state)
c.state = state
}
// CallSecurityCenter は警備センター警備員呼び出し。
func (c *SafeConsole) CallSecurityCenter(msg string) {
fmt.Println("call! " + msg)
}
// RecordLog は警備センター記録。
func (c *SafeConsole) RecordLog(msg string) {
fmt.Println("record ... " + msg)
}
// PressUse は「金庫使用」ボタンが押されたことのシミュレート
// (Java版SafeFrame.actionPerformedのbuttonUse分岐相当)。
func (c *SafeConsole) PressUse() {
fmt.Println("[金庫使用ボタン]")
c.state.DoUse(c)
}
// PressAlarm は「非常ベル」ボタンが押されたことのシミュレート。
func (c *SafeConsole) PressAlarm() {
fmt.Println("[非常ベルボタン]")
c.state.DoAlarm(c)
}
// PressPhone は「通常通話」ボタンが押されたことのシミュレート。
func (c *SafeConsole) PressPhone() {
fmt.Println("[通常通話ボタン]")
c.state.DoPhone(c)
}
package main
import "fmt"
// 実行: go run ./GoF/patterns/State/go
//
// Java版Main.javaは0時〜23時のsetClock()呼び出しを1秒おきに無限ループで
// 回し続け、その間にユーザーがGUIのボタンを手で押す想定だった。このGo版に
// GUIは無いので、0時始まりで24時間を1周だけ回し、いくつかの時刻で
// ボタン操作(金庫使用/非常ベル/通常通話)をあらかじめ仕込んで自動的に
// 押す形にした。
//
// 出力からは、
// - 09:00と17:00をまたぐタイミングでState自身がChangeState()を呼び、
// 昼⇄夜の状態が切り替わること
// - 同じ「金庫使用」「非常ベル」「通常通話」という操作でも、状態(昼/夜)に
// よって結果が変わること(夜間の金庫使用は非常呼び出しになる、など)
//
// が確認できる。
func main() {
console := NewSafeConsole()
// hour -> その時刻に押すボタン。
presses := map[int]func(){
2: console.PressPhone, // 夜間: 通常通話 → 録音のみ
3: console.PressUse, // 夜間: 金庫使用 → 非常呼び出し
10: console.PressUse, // 昼間: 金庫使用 → 記録のみ
13: console.PressAlarm, // 昼間: 非常ベル → 通常の呼び出し
19: console.PressUse, // 夜間: 金庫使用 → 非常呼び出し
22: console.PressAlarm, // 夜間: 非常ベル → 非常の呼び出し
}
for hour := 0; hour < 24; hour++ {
console.SetClock(hour)
if press, ok := presses[hour]; ok {
press()
}
}
fmt.Println("End.")
}
PHP
<?php
/**ステートパターン
*
* State: 状態ごとの振る舞いを定義するメソッドの集まり
* ConcreteState Stateで定義されたメソッドを具体的に実装
* Context:ConcreteStateを持ち、Stateを利用するためのAPIを持つ
*/
/**
* state
*/
interface Manual
{
public function behave();
}
/**
* concreteState
*/
class LadyManual implements Manual
{
public function behave()
{
var_dump("本当の親のように甘える");
}
}
class OldGuyManual implements Manual
{
public function behave()
{
var_dump("持っているものを褒める");
}
}
class AngryManManual implements Manual
{
public function behave()
{
var_dump("子供らしく振舞う");
}
}
// Context :state自身
class StateContext
{
private $state;
public function __construct($age)
{
$this->setTargetAge($age);
}
// concreteStateを持つ
public function setTargetAge($age)
{
if ($age >= 20 && $age <= 40) {
$this->state = new LadyManual();
} elseif ($age >= 40 && $age <= 60) {
$this->state = new AngryManManual();
} elseif ($age > 60) {
$this->state = new OldGuyManual();
} else {
throw new Exception("そんな人よく知らんよ");
}
}
//concreteStateを利用するAPIを持つ
public function behave()
{
$this->state->behave();
}
}
function main()
{
$human = new StateContext(20);
$human->behave();
$human->setTargetAge(41);
$human->behave();
$human->setTargetAge(61);
$human->behave();
}
main();TypeScript
DayState/NightStateはJava版同様「privateコンストラクタ + static instance + static getInstance()」でシングルトンにする(Singleton/typescript版と同じ形)。互いのgetInstance()を呼び合う相互importになるが、参照はメソッド本体内(実行時)にしか現れないため循環しても問題ない。GUI(SafeFrame)はGo版と同じ考え方でSafeConsole(console.logベース、pressUse/pressAlarm/pressPhoneという明示メソッド)に置き換えている。
実行: npx tsx GoF/patterns/State/typescript/main.ts
// 実行方法は main.ts を参照(npx tsx GoF/patterns/State/typescript/main.ts)。
import type { Context } from "./context";
// State: 「状態」を表すインタフェース(Java版 State.java 相当)。
// ConcreteState(DayState/NightState)がこれをimplementsする。
export interface State {
doClock(context: Context, hour: number): void; // 時刻設定
doUse(context: Context): void; // 金庫使用
doAlarm(context: Context): void; // 非常ベル
doPhone(context: Context): void; // 通常通話
}
// 実行方法は main.ts を参照(npx tsx GoF/patterns/State/typescript/main.ts)。
import type { Context } from "./context";
import { NightState } from "./night_state";
import type { State } from "./state";
// DayState: ConcreteStateの一つ(昼間)。
//
// TypeScriptにはJavaのprivateコンストラクタがそのまま存在するので、
// 「privateコンストラクタ + static instance + static getInstance()」で
// シングルトンを表現する(Singleton/typescript版と同じ形)。
//
// day_state.tsとnight_state.tsは互いにgetInstance()を呼び合う(相互import)が、
// どちらも呼び出しはメソッド本体の中、つまり実行時まで評価されない箇所なので、
// モジュール読み込み時点での循環参照は問題にならない(Java版のDayState/NightStateが
// 互いを参照し合うのと同じ構造)。
export class DayState implements State {
private static instance: DayState | undefined;
private constructor() {}
// 唯一のインスタンスを得る。
static getInstance(): DayState {
if (!DayState.instance) {
DayState.instance = new DayState();
}
return DayState.instance;
}
// 時刻設定。9時未満・17時以降なら夜間へ遷移する。
doClock(context: Context, hour: number): void {
if (hour < 9 || 17 <= hour) {
context.changeState(NightState.getInstance());
}
}
// 金庫使用。昼間はただ記録するだけ。
doUse(context: Context): void {
context.recordLog("金庫使用(昼間)");
}
// 非常ベル。昼間は通常どおり警備センターへ連絡する。
doAlarm(context: Context): void {
context.callSecurityCenter("非常ベル(昼間)");
}
// 通常通話。昼間は警備センターへの通常の通話として扱う。
doPhone(context: Context): void {
context.callSecurityCenter("通常の通話(昼間)");
}
// 文字列表現(Java版 toString() 相当)。
toString(): string {
return "[昼間]";
}
}
// 実行方法は main.ts を参照(npx tsx GoF/patterns/State/typescript/main.ts)。
import type { Context } from "./context";
import { DayState } from "./day_state";
import type { State } from "./state";
// NightState: ConcreteStateの一つ(夜間)。DayStateと同じ理由でシングルトンにする。
export class NightState implements State {
private static instance: NightState | undefined;
private constructor() {}
static getInstance(): NightState {
if (!NightState.instance) {
NightState.instance = new NightState();
}
return NightState.instance;
}
// 時刻設定。9時以上17時未満になったら昼間へ遷移する。
doClock(context: Context, hour: number): void {
if (9 <= hour && hour < 17) {
context.changeState(DayState.getInstance());
}
}
// 金庫使用。夜間の金庫使用は非常事態として警備センターへ緊急連絡する。
doUse(context: Context): void {
context.callSecurityCenter("非常:夜間の金庫使用!");
}
// 非常ベル。夜間も警備センターへ連絡する。
doAlarm(context: Context): void {
context.callSecurityCenter("非常ベル(夜間)");
}
// 通常通話。夜間は録音だけしておき、警備員は呼ばない。
doPhone(context: Context): void {
context.recordLog("夜間の通話録音");
}
// 文字列表現(Java版 toString() 相当)。
toString(): string {
return "[夜間]";
}
}
// 実行方法は main.ts を参照(npx tsx GoF/patterns/State/typescript/main.ts)。
import type { State } from "./state";
// Context: Java版 Context.java(インタフェース)相当。
// State側からコールバックされる「状況」に対する操作をまとめる。
export interface Context {
setClock(hour: number): void; // 時刻の設定
changeState(state: State): void; // 状態変化
callSecurityCenter(msg: string): void; // 警備センター警備員呼び出し
recordLog(msg: string): void; // 警備センター記録
}
// 実行方法は main.ts を参照(npx tsx GoF/patterns/State/typescript/main.ts)。
//
// SafeConsole: ContextのConcreteState利用者(Java版 SafeFrame.java 相当)。
//
// 【GUIの置き換えについて】
// Java版のSafeFrameはAWTのFrame/Button/TextField/TextAreaでできたGUIで、
// ユーザーが「金庫使用」「非常ベル」「通常通話」「終了」ボタンを押すと
// actionPerformed(e)経由でState.doXxx(this)が呼ばれ、textClock/textScreenに
// 結果が描画される、という作りだった。
//
// このTypeScript版にGUIは無いので、
// - textClock/textScreenへの描画 → console.log (標準出力)
// - ボタン押下(金庫使用/非常ベル/通常通話) → pressUse/pressAlarm/pressPhone という
// 公開メソッド。main.ts側がタイマー代わりのforループの中で、
// 「このボタンを押す」を明示的に呼び出す
// に置き換えている(終了ボタンはループが自然に終わるだけなので用意していない)。
import type { Context } from "./context";
import { DayState } from "./day_state";
import type { State } from "./state";
export class SafeConsole implements Context {
// Java版同様、初期状態は昼間から始まる。
private state: State = DayState.getInstance();
// 時刻の設定。
setClock(hour: number): void {
const clockString = `現在時刻は${String(hour).padStart(2, "0")}:00`;
console.log(clockString);
this.state.doClock(this, hour);
}
// 状態変化。
changeState(state: State): void {
console.log(`${this.state}から${state}へ状態が変化しました。`);
this.state = state;
}
// 警備センター警備員呼び出し。
callSecurityCenter(msg: string): void {
console.log(`call! ${msg}`);
}
// 警備センター記録。
recordLog(msg: string): void {
console.log(`record ... ${msg}`);
}
// 「金庫使用」ボタンが押されたことのシミュレート
// (Java版SafeFrame.actionPerformedのbuttonUse分岐相当)。
pressUse(): void {
console.log("[金庫使用ボタン]");
this.state.doUse(this);
}
// 「非常ベル」ボタンが押されたことのシミュレート。
pressAlarm(): void {
console.log("[非常ベルボタン]");
this.state.doAlarm(this);
}
// 「通常通話」ボタンが押されたことのシミュレート。
pressPhone(): void {
console.log("[通常通話ボタン]");
this.state.doPhone(this);
}
}
// State パターン: 警備員室の金庫(昼/夜の状態切り替え) (Java版と同じ題材)
//
// 実行: npx tsx GoF/patterns/State/typescript/main.ts
//
// Java版Main.javaは0時〜23時のsetClock()呼び出しを1秒おきに無限ループで
// 回し続け、その間にユーザーがGUIのボタンを手で押す想定だった。この
// TypeScript版にGUIは無いので、0時始まりで24時間を1周だけ回し、いくつかの
// 時刻でボタン操作(金庫使用/非常ベル/通常通話)をあらかじめ仕込んで自動的に
// 押す形にした。
//
// 出力からは、
// - 09:00と17:00をまたぐタイミングでState自身がchangeState()を呼び、
// 昼⇄夜の状態が切り替わること
// - 同じ「金庫使用」「非常ベル」「通常通話」という操作でも、状態(昼/夜)に
// よって結果が変わること(夜間の金庫使用は非常呼び出しになる、など)
// が確認できる。
import { SafeConsole } from "./safe_console";
function main(): void {
const console_ = new SafeConsole();
// hour -> その時刻に押すボタン。
const presses: Record<number, () => void> = {
2: () => console_.pressPhone(), // 夜間: 通常通話 → 録音のみ
3: () => console_.pressUse(), // 夜間: 金庫使用 → 非常呼び出し
10: () => console_.pressUse(), // 昼間: 金庫使用 → 記録のみ
13: () => console_.pressAlarm(), // 昼間: 非常ベル → 通常の呼び出し
19: () => console_.pressUse(), // 夜間: 金庫使用 → 非常呼び出し
22: () => console_.pressAlarm(), // 夜間: 非常ベル → 非常の呼び出し
};
for (let hour = 0; hour < 24; hour++) {
console_.setClock(hour);
presses[hour]?.();
}
console.log("End.");
}
main();
Python
State/ContextはJavaのinterfaceに相当する言語機能が無いため抽象基底クラス(ABC)で表現(Composite/Proxy版と同じ方針)。DayState/NightStateはSingleton/python版と同じ「クラス変数(_instance) + クラスメソッド(get_instance)」でシングルトンにする。day_state.py/night_state.pyはimport night_stateのようにモジュールごとimportし、相手クラスへの参照をメソッド呼び出し時まで遅延させることで循環importを回避している。GUI(SafeFrame)はGo/TS版と同じ考え方でSafeConsole(printベース、press_use/press_alarm/press_phoneという明示メソッド)に置き換えている。
実行: python3 GoF/patterns/State/python/main.py
"""State: 「状態」を表すインタフェース(Java版 State.java 相当)。
単体では実行不可。エントリポイントは main.py (python3 main.py)。
PythonにはJavaのinterfaceに相当する言語機能はないため、Composite/Proxy版と同じく
抽象基底クラス(ABC, abcモジュール)で表す。
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from context import Context
class State(ABC):
@abstractmethod
def do_clock(self, context: "Context", hour: int) -> None: ... # 時刻設定
@abstractmethod
def do_use(self, context: "Context") -> None: ... # 金庫使用
@abstractmethod
def do_alarm(self, context: "Context") -> None: ... # 非常ベル
@abstractmethod
def do_phone(self, context: "Context") -> None: ... # 通常通話
"""DayState: ConcreteStateの一つ(昼間)。
単体では実行不可。エントリポイントは main.py (python3 main.py)。
Java版の「private コンストラクタ + private static singleton フィールド +
public static getInstance()」を、Singleton/python版と同じ「クラス変数(_instance) +
クラスメソッド(get_instance)」で再現する(Pythonにprivateコンストラクタという
言語機能は無いので、強制力は無く規約止まり。Singleton/python版のdocstringと同じ対比)。
day_state.pyとnight_state.pyは互いのget_instance()を呼び合う(相互依存)。
`from night_state import NightState` のようなfrom-import はモジュール実行の
その時点で相手側の属性が定義済みである必要があり、循環importで失敗しうる。
そのため `import night_state` の形でモジュールそのものを掴んでおき、実際の属性
アクセス(night_state.NightState)はdo_clockが呼ばれる実行時まで遅延させている
(TypeScript版の相互importに関するコメントと同じ問題への対処)。
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import night_state
from state import State
if TYPE_CHECKING:
from context import Context
class DayState(State):
_instance: "DayState | None" = None
def __init__(self) -> None:
pass
@classmethod
def get_instance(cls) -> "DayState":
"""唯一のインスタンスを得る。"""
if cls._instance is None:
cls._instance = cls()
return cls._instance
def do_clock(self, context: "Context", hour: int) -> None:
"""時刻設定。9時未満・17時以降なら夜間へ遷移する。"""
if hour < 9 or 17 <= hour:
context.change_state(night_state.NightState.get_instance())
def do_use(self, context: "Context") -> None:
"""金庫使用。昼間はただ記録するだけ。"""
context.record_log("金庫使用(昼間)")
def do_alarm(self, context: "Context") -> None:
"""非常ベル。昼間は通常どおり警備センターへ連絡する。"""
context.call_security_center("非常ベル(昼間)")
def do_phone(self, context: "Context") -> None:
"""通常通話。昼間は警備センターへの通常の通話として扱う。"""
context.call_security_center("通常の通話(昼間)")
def __str__(self) -> str:
"""文字列表現(Java版 toString() 相当)。"""
return "[昼間]"
"""NightState: ConcreteStateの一つ(夜間)。DayStateと同じ理由でシングルトンにする。
単体では実行不可。エントリポイントは main.py (python3 main.py)。
day_state.pyとの循環参照の扱いはday_state.pyのdocstringを参照。
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import day_state
from state import State
if TYPE_CHECKING:
from context import Context
class NightState(State):
_instance: "NightState | None" = None
def __init__(self) -> None:
pass
@classmethod
def get_instance(cls) -> "NightState":
"""唯一のインスタンスを得る。"""
if cls._instance is None:
cls._instance = cls()
return cls._instance
def do_clock(self, context: "Context", hour: int) -> None:
"""時刻設定。9時以上17時未満になったら昼間へ遷移する。"""
if 9 <= hour < 17:
context.change_state(day_state.DayState.get_instance())
def do_use(self, context: "Context") -> None:
"""金庫使用。夜間の金庫使用は非常事態として警備センターへ緊急連絡する。"""
context.call_security_center("非常:夜間の金庫使用!")
def do_alarm(self, context: "Context") -> None:
"""非常ベル。夜間も警備センターへ連絡する。"""
context.call_security_center("非常ベル(夜間)")
def do_phone(self, context: "Context") -> None:
"""通常通話。夜間は録音だけしておき、警備員は呼ばない。"""
context.record_log("夜間の通話録音")
def __str__(self) -> str:
"""文字列表現(Java版 toString() 相当)。"""
return "[夜間]"
"""Context: Java版 Context.java(インタフェース)相当。
単体では実行不可。エントリポイントは main.py (python3 main.py)。
State側からコールバックされる「状況」に対する操作をまとめる。Java版のinterfaceに
相当する言語機能はないため、State.pyと同じく抽象基底クラス(ABC)で表す。
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from state import State
class Context(ABC):
@abstractmethod
def set_clock(self, hour: int) -> None: ... # 時刻の設定
@abstractmethod
def change_state(self, state: "State") -> None: ... # 状態変化
@abstractmethod
def call_security_center(self, msg: str) -> None: ... # 警備センター警備員呼び出し
@abstractmethod
def record_log(self, msg: str) -> None: ... # 警備センター記録
"""SafeConsole: ContextのConcreteState利用者(Java版 SafeFrame.java 相当)。
単体では実行不可。エントリポイントは main.py (python3 main.py)。
【GUIの置き換えについて】
Java版のSafeFrameはAWTのFrame/Button/TextField/TextAreaでできたGUIで、
ユーザーが「金庫使用」「非常ベル」「通常通話」「終了」ボタンを押すと
actionPerformed(e)経由でState.doXxx(this)が呼ばれ、textClock/textScreenに
結果が描画される、という作りだった。
このPython版にGUIは無いので、
- textClock/textScreenへの描画 -> print() (標準出力)
- ボタン押下(金庫使用/非常ベル/通常通話) -> press_use/press_alarm/press_phone という
公開メソッド。main.py側がタイマー代わりのforループの中で、
「このボタンを押す」を明示的に呼び出す
に置き換えている(終了ボタンはループが自然に終わるだけなので用意していない)。
"""
from __future__ import annotations
from context import Context
from day_state import DayState
from state import State
class SafeConsole(Context):
def __init__(self) -> None:
self._state: State = DayState.get_instance() # Java版同様、初期状態は昼間から始まる
def set_clock(self, hour: int) -> None:
"""時刻の設定。"""
clock_string = f"現在時刻は{hour:02d}:00"
print(clock_string)
self._state.do_clock(self, hour)
def change_state(self, state: State) -> None:
"""状態変化。"""
print(f"{self._state}から{state}へ状態が変化しました。")
self._state = state
def call_security_center(self, msg: str) -> None:
"""警備センター警備員呼び出し。"""
print(f"call! {msg}")
def record_log(self, msg: str) -> None:
"""警備センター記録。"""
print(f"record ... {msg}")
def press_use(self) -> None:
"""「金庫使用」ボタンが押されたことのシミュレート
(Java版SafeFrame.actionPerformedのbuttonUse分岐相当)。
"""
print("[金庫使用ボタン]")
self._state.do_use(self)
def press_alarm(self) -> None:
"""「非常ベル」ボタンが押されたことのシミュレート。"""
print("[非常ベルボタン]")
self._state.do_alarm(self)
def press_phone(self) -> None:
"""「通常通話」ボタンが押されたことのシミュレート。"""
print("[通常通話ボタン]")
self._state.do_phone(self)
"""State パターン: 警備員室の金庫(昼/夜の状態切り替え) (Java版と同じ題材)
実行:
python3 main.py
(もしくはリポジトリルートから python3 GoF/patterns/State/python/main.py)
Java版Main.javaは0時〜23時のsetClock()呼び出しを1秒おきに無限ループで回し
続け、その間にユーザーがGUIのボタンを手で押す想定だった。このPython版に
GUIは無いので、0時始まりで24時間を1周だけ回し、いくつかの時刻でボタン操作
(金庫使用/非常ベル/通常通話)をあらかじめ仕込んで自動的に押す形にした。
出力からは、
- 09:00と17:00をまたぐタイミングでState自身がchange_state()を呼び、
昼⇄夜の状態が切り替わること
- 同じ「金庫使用」「非常ベル」「通常通話」という操作でも、状態(昼/夜)に
よって結果が変わること(夜間の金庫使用は非常呼び出しになる、など)
が確認できる。
"""
from __future__ import annotations
from safe_console import SafeConsole
def main() -> None:
console = SafeConsole()
# hour -> その時刻に押すボタン。
presses = {
2: console.press_phone, # 夜間: 通常通話 → 録音のみ
3: console.press_use, # 夜間: 金庫使用 → 非常呼び出し
10: console.press_use, # 昼間: 金庫使用 → 記録のみ
13: console.press_alarm, # 昼間: 非常ベル → 通常の呼び出し
19: console.press_use, # 夜間: 金庫使用 → 非常呼び出し
22: console.press_alarm, # 夜間: 非常ベル → 非常の呼び出し
}
for hour in range(24):
console.set_clock(hour)
press = presses.get(hour)
if press is not None:
press()
print("End.")
if __name__ == "__main__":
main()