Menu
Coddy logo textTech

Stateパターン

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

Stateパターンを使用すると、オブジェクトの内部状態が変化したときにその振る舞いを変更でき、まるでオブジェクトのクラスが変わったかのように見せることができます。Template Methodがアルゴリズムの手順を制御するのに対し、Stateは状態ごとの振る舞いを個別のオブジェクトにカプセル化し、current stateに委譲します。

Goでは、stateインターフェースを定義し、異なるbehaviorをImplementする具体的な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"
}

同じメソッド呼び出しでも、current state に応じて異なる結果が生成されます。

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

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

Stateは、注文処理ワークフロー、UIコンポーネント、接続ハンドラーなど、behaviorがcurrent stateに完全に依存する、明確に異なる動作モードを持つオブジェクトに最適です。

challenge icon

チャレンジ

簡単

State パターンを使ってチケットサポートシステムを構築しましょう!チケットが開かれた状態から、対応中の状態、解決済みの状態へとさまざまな段階を移行し、各状態によって可能なアクションと次に起こることが決まるサポートチケットを作成します。

コードを次の3つのファイルに分けて整理します。

  • state.go: チケットのライフサイクルの各段階を表す state interface と具体的な状態を Define します。

    TicketState interface を作成し、チケットを処理して、次の状態へ遷移させる可能性のあるメソッド Handle(t *Ticket) string を定義します。

    次の3つの状態を Implement します。

    • OpenState: handled されると、チケットを InProgressState に遷移させ、Ticket opened, assigning to support team を返します
    • InProgressState: handled されると、ResolvedState に遷移し、Working on ticket, issue resolved を返します
    • ResolvedState: handled されると、同じ状態にとどまり、Ticket already resolved を返します
  • ticket.go: current state を保持し、その behavior を委譲するチケット構造体を作成します。

    ID field(string)と state field(TicketState)を持つ Ticket struct を構築します。次のメソッドを追加します。

    • チケットの current state を変更する SetState(s TicketState)
    • current state の Handle method に処理を委譲する Process() string

    OpenState で開始するチケットを returns する NewTicket(id string) *Ticket constructor を作成します。

  • main.go: チケットの current state に応じて、同じアクションが異なる結果を生成することを示します。

    チケット ID と、チケットを Process する回数を Read します。その ID で新しいチケットを作成し、指定された回数だけ Process() を呼び出して、各 result を別々の行に出力します。

次の入力が提供されます。

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

たとえば、次の入力が given された場合:

TKT-001
3

出力は次のようになります。

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

また、次の入力が given された場合:

TKT-500
5

出力は次のようになります。

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

また、次の入力が given された場合:

ISSUE-42
1

出力は次のようになります。

Ticket opened, assigning to support team

同じチケットに対して Process() を呼び出すと、毎回異なる result が生成されることに注目してください。チケットが各 state を移行するにつれて、その behavior が changes します。いったん resolved になると、何度 Process しても resolved のままです。チケットオブジェクト自体が behavior を changes しているように見えますが、実際には異なる state オブジェクトに delegates しています。

自分で試してみよう

package main

import (
	"fmt"
)

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

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

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

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

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

自分で練習してみよう: Goオンラインコンパイラ