Menu
Coddy logo textTech

Stateパターン

CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 95/107。

Stateパターンは、オブジェクトの内部状態が変化したときにその振る舞いを変えることを可能にし、まるでオブジェクトのクラスが変わったかのように見せます。Template Methodがアルゴリズムのステップを制御するのに対し、Stateは状態固有の振る舞いを個別のオブジェクトにカプセル化し、現在の状態に委譲します。

Goでは、stateインターフェースを定義し、異なる振る舞いを実装する具体的なステートを作成します:

type State interface {
    Handle(d *Document) string
}

type Document struct {
    state State
}

func (d *Document) SetState(s State) {
    d.state = s
}

func (d *Document) Publish() string {
    return d.state.Handle(d)
}

各状態は、何が起こるかと次にどの状態になるかを決定します:

type DraftState struct{}

func (s DraftState) Handle(d *Document) string {
    d.SetState(ModerationState{})
    return "Draft submitted for moderation"
}

type ModerationState struct{}

func (s ModerationState) Handle(d *Document) string {
    d.SetState(PublishedState{})
    return "Moderation approved, now published"
}

type PublishedState struct{}

func (s PublishedState) Handle(d *Document) string {
    return "Already published"
}

同じメソッド呼び出しでも、現在の状態に基づいて異なる結果が生成されます:

doc := &Document{state: DraftState{}}

fmt.Println(doc.Publish())  // 下書きがモデレーションに提出されました
fmt.Println(doc.Publish())  // モデレーションが承認され、公開されました
fmt.Println(doc.Publish())  // すでに公開されています

Stateは、注文処理ワークフロー、UIコンポーネント、または振る舞いが現在の状態に完全に依存する接続ハンドラーなど、明確な動作モードを持つオブジェクトに最適です。

challenge icon

チャレンジ

簡単

State パターンを使用して、チケットサポートシステムを構築しましょう!チケットがオープンされ、作業中になり、解決されるという異なるステージを経て移動するサポートチケットを作成します。各状態(ステート)によって、どのようなアクションが可能か、次に何が起こるかが決定されます。

コードは以下の3つのファイルに分けて構成します:

  • state.go: ステートインターフェースと、チケットのライフサイクルの各段階を表す具体的なステートを定義します。

    チケットを処理し、場合によっては次の状態に遷移させる Handle(t *Ticket) string メソッドを持つ TicketState インターフェースを作成します。

    以下の3つのステートを実装してください:

    • OpenState — ハンドルされると、チケットを InProgressState に遷移させ、"Ticket opened, assigning to support team" を返します。
    • InProgressState — ハンドルされると、ResolvedState に遷移させ、"Working on ticket, issue resolved" を返します。
    • ResolvedState — ハンドルされると、同じ状態を維持し、"Ticket already resolved" を返します。
  • ticket.go: 現在の状態を保持し、振る舞いをその状態に委譲するチケット構造体を作成します。

    ID フィールド(string)と state フィールド(TicketState)を持つ Ticket 構造体を構築します。以下のメソッドを追加してください:

    • SetState(s TicketState): チケットの現在の状態を変更します。
    • Process() string: 現在の状態の Handle メソッドに処理を委譲します。

    OpenState で開始されるチケットを返す NewTicket(id string) *Ticket コンストラクタを作成します。

  • main.go: チケットの現在の状態に基づいて、同じアクションがどのように異なる結果を生むかを実演します。

    チケット ID と、チケットを処理する回数を読み取ります。その ID で新しいチケットを作成し、指定された回数だけ Process() を呼び出し、各結果を個別の行に出力します。

以下の入力が提供されます:

  • 1行目: チケット ID
  • 2行目: チケットを処理する回数

例えば、以下の場合:

TKT-001
3

出力は以下のようになります:

Ticket opened, assigning to support team
Working on ticket, issue resolved
Ticket already resolved

また、以下の場合:

TKT-500
5

出力は以下のようになります:

Ticket opened, assigning to support team
Working on ticket, issue resolved
Ticket already resolved
Ticket already resolved
Ticket already resolved

また、以下の場合:

ISSUE-42
1

出力は以下のようになります:

Ticket opened, assigning to support team

同じチケットに対して Process() を呼び出しても、毎回異なる結果が得られることに注目してください。チケットの状態が遷移するにつれて、その振る舞いが変化します。一度解決(resolved)されると、何度処理しても解決されたままになります。チケットオブジェクトが自身の振る舞いを変えているように見えますが、実際には異なるステートオブジェクトに処理を委譲しているのです!

チートシート

State パターンは、オブジェクトの内部状態が変化したときに、その振る舞いを変更できるようにします。オブジェクトは、異なる状態を表す個別の状態オブジェクトに振る舞いを委譲します。

状態インターフェースを定義します:

type State interface {
    Handle(d *Document) string
}

現在の状態を保持するコンテキストオブジェクトを作成します:

type Document struct {
    state State
}

func (d *Document) SetState(s State) {
    d.state = s
}

func (d *Document) Publish() string {
    return d.state.Handle(d)
}

状態固有の振る舞いをカプセル化する具体的な状態を実装します:

type DraftState struct{}

func (s DraftState) Handle(d *Document) string {
    d.SetState(ModerationState{})
    return "Draft submitted for moderation"
}

type ModerationState struct{}

func (s ModerationState) Handle(d *Document) string {
    d.SetState(PublishedState{})
    return "Moderation approved, now published"
}

type PublishedState struct{}

func (s PublishedState) Handle(d *Document) string {
    return "Already published"
}

各状態は何が起こるかを決定し、次の状態へと遷移します:

doc := &Document{state: DraftState{}}

fmt.Println(doc.Publish())  // 下書きがモデレーションに提出されました
fmt.Println(doc.Publish())  // モデレーションが承認され、公開されました
fmt.Println(doc.Publish())  // すでに公開されています

State パターンは、振る舞いが現在の状態に完全に依存する、明確な動作モードを持つオブジェクトに最適です。

自分で試してみよう

package main

import (
	"fmt"
)

func main() {
	// 入力を読み込む
	var ticketID string
	var numProcesses int
	fmt.Scanln(&ticketID)
	fmt.Scanln(&numProcesses)

	// TODO: 指定されたIDで新しいチケットを作成する

	// TODO: 指定された回数だけチケットを処理し、
	// 各結果を個別の行に出力する
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン