Goでデザインパターン (Stateパターン)

Goデザインパターン

Stateパターンとは

振る舞いに関するデザインパターンの一つ。あるオブジェクトの状態に依存した振る舞いを、各状態オブジェクトに分割し整理する。

長所

  • 状態毎の振る舞いをそれぞれ別のクラスにして整理できる。(単一責任の原則)
  • 既存の状態クラスやコンテキストクラスを変更せずに、新たな状態クラスを追加することができる。(開放閉鎖の原則)
  • 状態別の処理を行うための条件分離を削減できるため、コンテキストクラスを簡素化できる。

短所

  • 状態が数個しかない場合や状態の追加等が発生しない場合は、パターン適用は過剰になる可能性がある。

利用場面

  • 現在の状態に依存した振る舞いをするオブジェクトが存在し、その状態数が多く、状態固有の処理に変更が頻繁に起こりえる場合。
  • クラスのフィールドの現在の値に応じた処理をさせるため、膨大な条件分岐を必要とする場合。
  • 状態特有の処理に似たような処理が共通して現れる場合。
    (Stateパターンでは状態クラスの階層を作成することができるため、共通コードを基底抽象クラスに抽出して重複を削減できる。)

クラス図

Stateパターン

Context

複数種ある具象Stateオブジェクトのいずれか一つの参照を保持する。状態固有の作業はすべて保持しいている具象Stateオブジェクトに委任される。具象StateオブジェクトとはStateインタフェースを通じて通信を行う。

State

各種の状態に共通した振る舞いのインターフェースを宣言する。宣言された振る舞いは各具象Stateで状態固有の振る舞いが実装されることになる。

Concrete States

具体的な状態を表すオブジェクト。Stateインタフェースで宣言された振る舞いを実装する。
Contextの参照を保持し、Contextから必要な情報を取得するように実装することも可能。
また似たような処理を抽出した抽象状態オブジェクトを中間層として作成することも可能。

実装例

Context

package main

import (
	"fmt"
)

// Contextオブジェクト
// 現在の状態を表すStateオブジェクトの参照を保持する。
type Ticket struct {
	title string
	state State
}

func NewTicket(title string) *Ticket {
	return &Ticket{
		title: title,
		state: GetOpneInstance(),
	}
}

func (t *Ticket) ChangeState(state State) {
	fmt.Printf("change state: '%s' -> '%s'\n", t.state.GetState(), state.GetState())
	t.state = state
}

// 状態変更リクエストの処理をStateオブジェクトに委任する。
// -> 現在の状態に適切な処理をContextで判断する必要がない。
func (t *Ticket) Approve() {
	t.state.Approve(t)
}

func (t *Ticket) Reject() {
	t.state.Reject(t)
}

func (t *Ticket) Close() {
	t.state.Close(t)
}

func (t *Ticket) ShowState() {
	fmt.Printf("<< Current State: %s >>\n", t.state.GetState())
}

State

package main

// 全てのStateオブジェクトが実装すべきメソッドを宣言する。
type State interface {
	Approve(ticket *Ticket)
	Reject(ticket *Ticket)
	Close(ticket *Ticket)
	GetState() string
}

Concrete States

package main

var open *Open

type Open struct {}

// Stateオブジェクトはインスタンスフィールドを持たない。
// そのためシングルトンパターンを利用して実装することが可能。(必須ではない)
func init() {
	open = &Open{}
}

// 今回の実装例ではApprove()...の引数にContextを渡しているが、Stateオブジェクト生成時にContextの参照を保持するようにもできる。
// その場合はシングルトンは使えない。
func GetOpneInstance() *Open {
	return open
}

// Contesxtオブジェクトの状態変更メソッドを利用して、現在の状態に応じた適切な状態変更を行う。
// 状態遷移の責任を具象Stateオブジェクトが担うことになる。
// そのため他の具象Stateオブジェクトへの依存が必要となる。
func (o *Open) Approve(ticket *Ticket) {
	// Open -> In Progress
	ticket.ChangeState(GetInProgressInstance())
}

func (o *Open) Reject(ticket *Ticket) {
	// Open -> Close
	ticket.ChangeState(GetCloseInstance())
}

func (o *Open) Close(ticket *Ticket) {
	// Open -> Close
	ticket.ChangeState(GetCloseInstance())
}

func (o *Open) GetState() string {
	return "Open"
}
package main

var inProgress *InProgress

type InProgress struct {}

func init() {
	inProgress = &InProgress{}
}

func GetInProgressInstance() *InProgress {
	return inProgress
}

func (i *InProgress) Approve(ticket *Ticket) {
	// In Progress -> Close
	ticket.ChangeState(GetCloseInstance())
}

func (i *InProgress) Reject(ticket *Ticket) {
	// In Progress -> Open
	ticket.ChangeState(GetOpneInstance())
}

func (i *InProgress) Close(ticket *Ticket) {
	// In Progress -> Close
	ticket.ChangeState(GetCloseInstance())
}

func (i *InProgress) GetState() string {
	return "In Progress"
}
package main

import (
	"fmt"
)

var close *Close

type Close struct {}

func init() {
	close = &Close{}
}

func GetCloseInstance() *Close {
	return close
}

func (c *Close) Approve(ticket *Ticket) {
	fmt.Println("this ticket is already closed.")
}

func (c *Close) Reject(ticket *Ticket) {
	// Close -> In Progress
	ticket.ChangeState(GetInProgressInstance())
}

func (c *Close) Close(ticket *Ticket) {
	fmt.Println("this ticket is already closed.")
}

func (c *Close) GetState() string {
	return "Close"
}

動作確認

package main

import "fmt"

func main() {
	ticket := NewTicket("Test Ticket")

	// Open
	ticket.ShowState()
	fmt.Println()

	// Open -> In Progress
	ticket.Approve()
	ticket.ShowState()
	fmt.Println()

	// In Progress -> Close
	ticket.Approve()
	ticket.ShowState()
	fmt.Println()

	// Close -> Open
	ticket.Reject()
	ticket.Reject()
	ticket.ShowState()
	fmt.Println()

	// Open -> Close
	ticket.Close()
	ticket.ShowState()
	fmt.Println()

	// already closed
	ticket.Approve()
	ticket.ShowState()
}
>> go run .
<< Current State: Open >>

change state: 'Open' -> 'In Progress' 
<< Current State: In Progress >>      

change state: 'In Progress' -> 'Close'
<< Current State: Close >>

change state: 'Close' -> 'In Progress'
change state: 'In Progress' -> 'Open' 
<< Current State: Open >>

change state: 'Open' -> 'Close'       
<< Current State: Close >>

this ticket is already closed.
<< Current State: Close >>

コメント

タイトルとURLをコピーしました