bykof/statefulを使って、Golangで状態遷移(State machine)を扱う

## はじめに
ビジネスロジックを実装していると、状態の管理はいつもついて回ります。
この辺のgolangでの実装について、[bykof/stateful](https://github.com/bykof/stateful)がいい感じだったので紹介します。

## [bykof/stateful](https://github.com/bykof/stateful)の使い方

例として、以下のようなとある注文システムの状態遷移図を実現してみます

![image.png](http://www.plantuml.com/plantuml/png/SoWkIImgAStDuKh9J2zABCXGo2zAIIrIgERYT79rz_JJjVFCSmQgY5R8IKnApR5L2axDIKqk2IZAJobDBb5IYBM6A_G7QKnDWQi4ynLICek0wlEBAY5g8QRXK0nH0fNIjGER2rVXd9YNd9g3LqbmHdu1q6L9EQcv1DHb5Y0v7UuiX0vhBgvQBeVKl1HWo000)

### 1. 状態を定義する

まずは状態遷移図の○部分、状態の定義を `stateful.DefaultState` を使って行います。

```golang:statemachine/definition.go
import "github.com/bykof/stateful"

var (
        BEGIN    = stateful.DefaultState("BEGIN")
        InCart   = stateful.DefaultState("InCart")
        Ordered  = stateful.DefaultState("Ordered")
        Canceled = BEGIN
        Shipped  = BEGIN
)
```

そして `State() state stateful.State` , `SetState(state stateful.State) error` のinterface実装を行います。

```golang:statemachine/order_state.go
type OrderState struct {
        state   stateful.State
        deposit int
}

func NewOrderState(deposit int) OrderState {
        return OrderState{
                state:   BEGIN,
                deposit: deposit,
        }
}

func (s *OrderState) State() stateful.State {
        return s.state
}

//this will be called after calling a transition function
func (s *OrderState) SetState(state stateful.State) error {
        s.state = state
        return nil
}
```

この実装がstateを持つobjectになります。

### 2. 状態遷移を定義する

状態遷移は以下のように、 `stateful.StateMachine` の `AddTransition` を利用します。
statefulパッケージではstate machineに1で定義したobjectを渡して使うため、登録する関数はそのobjectのメソッドで定義するのが扱いやすそうです。

```golang:statemachine/state_machine.go
        stateMachine := stateful.StateMachine{
                StatefulObject: object,
        }
        //Add transition
        stateMachine.AddTransition(
                object.Order,//状態遷移時に実行する関数
                stateful.States{BEGIN, InCart},//状態遷移元
                stateful.States{Ordered},//状態遷移先
        )
```

状態遷移時に実行する関数では、 `stateful.TransitionArguments` というinterfaceが引数として渡されます。これは、状態遷移時を走らせる時に呼ぶ `Run`関数で渡す、任意の構造体となります。なので関数内でcastして使います。

errなしの場合は次に遷移させる状態を返します。

```golang:statemachine/state_machine.go
func (s OrderState) Order(transitionArguments stateful.TransitionArguments) (stateful.State, error) {
        //transition argument is in Run parameter
        p, ok := transitionArguments.(*Product)
        if !ok {
                return nil, errors.New("Invalid argument")
        }

        if s.deposit < p.Fee {
                return nil, errors.New("The deposit is not enough")
        }
        s.deposit -= p.Fee
        return Ordered, nil//遷移後の状態を返す
}
```

### 3. 状態遷移の定義を確認する。

1, 2で状態遷移が作成できました。これらが正しく作成できているかは、 `github.com/bykof/stateful/statefulGraph` を使って確認ができます。

```golang:statemachine/state_machine.go
        //check graph
        stateMachineGraph := statefulGraph.StateMachineGraph{StateMachine: machine}//machineは2で作ったもの
        _ = stateMachineGraph.DrawGraph()
```

こんな感じに出力されます。PlantUMLで使用したい場合は、 `DrawGraphWithName` を利用ください

```
digraph  {
	BEGIN->InCart[ label="SelectProduct" ];
	BEGIN->Ordered[ label="Order" ];
	InCart->Ordered[ label="Order" ];
	Ordered->BEGIN[ label="Cancel" ];
	Ordered->BEGIN[ label="ShopProblem" ];
	Ordered->BEGIN[ label="Ship" ];
	BEGIN;
	InCart;
	Ordered;

}
```

また、AddTransitionは同じ状態からの遷移に対して複数設定可能なので、使い勝手が良さそうです。

```golang:statemachine/state_machine.go
        stateMachine.AddTransition(
                object.Cancel,
                stateful.States{Ordered},
                stateful.States{Canceled},
        )

        stateMachine.AddTransition(
                object.ShopProblem,
                stateful.States{Ordered},
                stateful.States{Canceled},
        )
```

### 4. 状態遷移を回す
あとは2で作ったstate machineに対して `Run` を実行するだけです。
状態遷移の識別は `AddTransaction` で指定した関数で行われます。

```golang:main.go
        err = machine.Run(order.Order, &product1)
        if err != nil {
                fmt.Printf("Error")
                return
        }
```

## sample code
本家のsampleは[こちら](https://github.com/bykof/stateful/tree/master/examples)

この記事に記載したコードは[こちら](https://github.com/developer-kikikaikai/stateful_sample)に置いてあります。

## 参考
[Programming business processes in Golang](https://medium.com/swlh/programming-business-processes-in-golang-f3612108d16b)