Documentation
¶
Overview ¶
Package fsm provides small finite state machine primitives for polymorphic state objects.
The central model is: the same business object delegates a behavior to its current State, and different concrete states implement that behavior differently. For example, UnpaidOrderState.Pay may perform the payment, while PaidOrderState.Pay returns an error because the order is already paid.
The package owns transition definitions and target state construction after a behavior has succeeded. The consumer owns the business object, its current concrete state, state-specific behavior methods, and the side effects that happen during a transition.
A common consumer pattern is to define a base state whose behavior methods return default errors, then embed it in concrete states and override only the methods supported by that state. That keeps state-specific behavior in concrete state types instead of adding if/else or switch checks to the business object.
The usual setup flow is:
- create a StateMachine with NewStateMachine
- register every state builder used by transitions
- add transitions with AddTransition
- call Check during application startup or tests
- call Freeze, or register the machine with RegisterStateMachine, before using it at runtime
The usual runtime flow is:
- the business object implements StateContext
- each business method type-asserts CurrentState to the business state interface and executes behavior with natural parameters and return values
- if behavior succeeds, call Transit with the business object, a RuntimeStateMachine, and action, usually through a private helper on the business object
- Transit gets candidate edges from the RuntimeStateMachine, evaluates their conditions, builds the matching state, sets its context, and delegates state replacement to StateContext.SetState
Keep state-machine lookup and transition plumbing out of public business method signatures. Prefer methods such as Order.Pay() that delegate to the current state and then transition after the behavior succeeds.
StateContext.SetState is intentionally implemented by the business object so it can type-check the next state and keep any domain side effects local.
Do not use HasTransition as the primary business permission check before calling the current state's behavior method. That bypasses state polymorphism: an invalid behavior should normally be rejected by the concrete state method itself.
A Condition is a transition guard evaluated by Transit to decide whether a specific from-state to to-state edge is allowed. A nil condition is unconditional. If transitions exist for the current state and action but no condition matches, Transit leaves the current state unchanged.
StateMachine protects its transition and builder tables with a mutex and can be frozen into the read-only RuntimeStateMachine surface, but it does not synchronize the consumer's StateContext. The consumer is responsible for any concurrency control around its business object.
Index ¶
- func RegisterStateMachine(sm RuntimeStateMachine) error
- func Transit(context StateContext, sm RuntimeStateMachine, action Action) error
- type Action
- type Condition
- type RuntimeStateMachine
- type SimpleState
- type State
- type StateBuilder
- type StateBuilderError
- type StateContext
- type StateLabel
- type StateMachine
- type StateMachineCheckError
- type StateMachineDefinitionError
- type Transition
- type TransitionError
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RegisterStateMachine ¶
func RegisterStateMachine(sm RuntimeStateMachine) error
func Transit ¶ added in v0.10.0
func Transit(context StateContext, sm RuntimeStateMachine, action Action) error
Transit executes the standard transition flow for a StateContext.
Types ¶
type Condition ¶ added in v0.6.2
type Condition func(StateContext) bool
Condition guards whether a specific transition edge is allowed.
type RuntimeStateMachine ¶ added in v0.10.1
type RuntimeStateMachine interface {
Name() string // get the state machine name
HasTransition(StateLabel, Action) bool // check if has transition from a state by action
Transitions(StateLabel, Action) []Transition // get transition edges from a state by action
BuildState(StateLabel) (State, error) // build a state by label
Check() error // check the completeness of States and Transitions
}
RuntimeStateMachine is the read-only state machine surface used at runtime.
func GetStateMachine ¶
func GetStateMachine(name string) (RuntimeStateMachine, bool)
func MustGetStateMachine ¶
func MustGetStateMachine(name string) RuntimeStateMachine
type SimpleState ¶
type SimpleState struct {
// contains filtered or unexported fields
}
SimpleState is a reusable State implementation for concrete state structs that only need context and label storage.
func NewSimpleState ¶
func NewSimpleState(label StateLabel) *SimpleState
func (*SimpleState) Context ¶
func (s *SimpleState) Context() StateContext
func (*SimpleState) Label ¶
func (s *SimpleState) Label() StateLabel
func (*SimpleState) SetContext ¶
func (s *SimpleState) SetContext(context StateContext)
type State ¶
type State interface {
SetContext(StateContext) // set the state context
Context() StateContext // return the state context
Label() StateLabel // get the state label
}
State define the state interface.
type StateBuilderError ¶ added in v0.10.0
type StateBuilderError struct {
State StateLabel
Actual StateLabel
}
StateBuilderError reports a missing, nil, or label-mismatched state builder.
func NewStateBuilderError ¶ added in v0.10.0
func NewStateBuilderError(state StateLabel) *StateBuilderError
func NewStateBuilderMismatchError ¶ added in v0.10.1
func NewStateBuilderMismatchError(expected, actual StateLabel) *StateBuilderError
func (*StateBuilderError) Error ¶ added in v0.10.0
func (sbe *StateBuilderError) Error() string
type StateContext ¶
type StateContext interface {
CurrentState() State // get the current state
SetState(next State) error // replace the current state
}
StateContext define the state context interface.
type StateMachine ¶
type StateMachine interface {
RuntimeStateMachine
AddTransition(StateLabel, StateLabel, Action, Condition) error // add a transition
RegisterStateBuilder(StateLabel, StateBuilder) error // register a state
Freeze() (RuntimeStateMachine, error) // validate and make the machine read-only
}
StateMachine is the build-time state machine surface.
Example (OrderPayment) ¶
package main
import (
"errors"
"fmt"
"github.com/go-jimu/components/fsm"
)
const (
exampleStateMachineName = "order"
exampleStateUnpaid fsm.StateLabel = "order.unpaid"
exampleStatePaid fsm.StateLabel = "order.paid"
exampleStatePartialRefunded fsm.StateLabel = "order.partial_refunded"
exampleStateCanceled fsm.StateLabel = "order.canceled"
exampleActionPay fsm.Action = "pay"
exampleActionRefund fsm.Action = "refund"
exampleActionCancel fsm.Action = "cancel"
)
type exampleOrder struct {
stateMachineName string
state exampleOrderState
paidAmount int
paymentMethod string
refundedAmount int
canceled bool
}
func (order *exampleOrder) remainingAmount() int {
return order.paidAmount - order.refundedAmount
}
func (order *exampleOrder) hasRemainingRefundAmount() bool {
return order.remainingAmount() > 0
}
func (order *exampleOrder) isFullyRefunded() bool {
return order.remainingAmount() == 0
}
type exampleOrderState interface {
fsm.State
Pay(amount int, method string) (string, error)
Refund(amount int) error
Cancel() error
}
type exampleBaseOrderState struct {
*fsm.SimpleState
}
type exampleUnpaidOrderState struct {
exampleBaseOrderState
}
type examplePaidOrderState struct {
exampleBaseOrderState
}
type examplePartialRefundedOrderState struct {
exampleBaseOrderState
}
type exampleCanceledOrderState struct {
exampleBaseOrderState
}
func newExampleOrder() *exampleOrder {
initial := newExampleUnpaidOrderState().(exampleOrderState)
order := &exampleOrder{
stateMachineName: exampleStateMachineName,
state: initial,
}
initial.SetContext(order)
return order
}
func (order *exampleOrder) CurrentState() fsm.State {
return order.state
}
func (order *exampleOrder) transition(action fsm.Action) error {
return fsm.Transit(order, fsm.MustGetStateMachine(order.stateMachineName), action)
}
func (order *exampleOrder) SetState(next fsm.State) error {
state, ok := next.(exampleOrderState)
if !ok {
return errors.New("next state is not an order state")
}
order.state = state
return nil
}
func (order *exampleOrder) Pay(amount int, method string) (string, error) {
receipt, err := order.CurrentState().(exampleOrderState).Pay(amount, method)
if err != nil {
return "", err
}
if err := order.transition(exampleActionPay); err != nil {
return "", err
}
return receipt, nil
}
func (order *exampleOrder) Refund(amount int) error {
if err := order.CurrentState().(exampleOrderState).Refund(amount); err != nil {
return err
}
return order.transition(exampleActionRefund)
}
func (order *exampleOrder) Cancel() error {
if err := order.CurrentState().(exampleOrderState).Cancel(); err != nil {
return err
}
return order.transition(exampleActionCancel)
}
func (state *exampleBaseOrderState) Pay(int, string) (string, error) {
return "", fmt.Errorf("%s cannot pay", state.Label())
}
func (state *exampleBaseOrderState) Refund(int) error {
return fmt.Errorf("%s cannot refund", state.Label())
}
func (state *exampleBaseOrderState) Cancel() error {
return fmt.Errorf("%s cannot cancel", state.Label())
}
func (state *exampleUnpaidOrderState) Pay(amount int, method string) (string, error) {
if amount <= 0 {
return "", errors.New("payment amount must be positive")
}
order := state.Context().(*exampleOrder)
order.paidAmount = amount
order.paymentMethod = method
return fmt.Sprintf("receipt:%s:%d", method, amount), nil
}
func (state *examplePaidOrderState) Pay(int, string) (string, error) {
return "", errors.New("order has already been paid")
}
func (state *examplePaidOrderState) Refund(amount int) error {
if amount <= 0 {
return errors.New("refund amount must be positive")
}
order := state.Context().(*exampleOrder)
if amount > order.remainingAmount() {
return errors.New("refund amount exceeds remaining amount")
}
order.refundedAmount += amount
return nil
}
func (state *examplePaidOrderState) Cancel() error {
order := state.Context().(*exampleOrder)
order.refundedAmount = order.paidAmount
order.canceled = true
return nil
}
func (state *examplePartialRefundedOrderState) Pay(int, string) (string, error) {
return "", errors.New("partially refunded order cannot be paid again")
}
func (state *examplePartialRefundedOrderState) Refund(amount int) error {
if amount <= 0 {
return errors.New("refund amount must be positive")
}
order := state.Context().(*exampleOrder)
if amount > order.remainingAmount() {
return errors.New("refund amount exceeds remaining amount")
}
order.refundedAmount += amount
return nil
}
func (state *examplePartialRefundedOrderState) Cancel() error {
order := state.Context().(*exampleOrder)
order.refundedAmount = order.paidAmount
order.canceled = true
return nil
}
func newExampleUnpaidOrderState() fsm.State {
return &exampleUnpaidOrderState{
exampleBaseOrderState: exampleBaseOrderState{
SimpleState: fsm.NewSimpleState(exampleStateUnpaid),
},
}
}
func newExamplePaidOrderState() fsm.State {
return &examplePaidOrderState{
exampleBaseOrderState: exampleBaseOrderState{
SimpleState: fsm.NewSimpleState(exampleStatePaid),
},
}
}
func newExamplePartialRefundedOrderState() fsm.State {
return &examplePartialRefundedOrderState{
exampleBaseOrderState: exampleBaseOrderState{
SimpleState: fsm.NewSimpleState(exampleStatePartialRefunded),
},
}
}
func newExampleCanceledOrderState() fsm.State {
return &exampleCanceledOrderState{
exampleBaseOrderState: exampleBaseOrderState{
SimpleState: fsm.NewSimpleState(exampleStateCanceled),
},
}
}
func newExampleOrderStateMachine() fsm.StateMachine {
sm := fsm.NewStateMachine(exampleStateMachineName)
sm.RegisterStateBuilder(exampleStateUnpaid, newExampleUnpaidOrderState)
sm.RegisterStateBuilder(exampleStatePaid, newExamplePaidOrderState)
sm.RegisterStateBuilder(exampleStatePartialRefunded, newExamplePartialRefundedOrderState)
sm.RegisterStateBuilder(exampleStateCanceled, newExampleCanceledOrderState)
sm.AddTransition(exampleStateUnpaid, exampleStatePaid, exampleActionPay, nil)
sm.AddTransition(exampleStatePaid, exampleStatePartialRefunded, exampleActionRefund, func(sc fsm.StateContext) bool {
return sc.(*exampleOrder).hasRemainingRefundAmount()
})
sm.AddTransition(exampleStatePaid, exampleStateCanceled, exampleActionRefund, func(sc fsm.StateContext) bool {
return sc.(*exampleOrder).isFullyRefunded()
})
sm.AddTransition(exampleStatePaid, exampleStateCanceled, exampleActionCancel, nil)
sm.AddTransition(exampleStatePartialRefunded, exampleStatePartialRefunded, exampleActionRefund, func(sc fsm.StateContext) bool {
return sc.(*exampleOrder).hasRemainingRefundAmount()
})
sm.AddTransition(exampleStatePartialRefunded, exampleStateCanceled, exampleActionRefund, func(sc fsm.StateContext) bool {
return sc.(*exampleOrder).isFullyRefunded()
})
sm.AddTransition(exampleStatePartialRefunded, exampleStateCanceled, exampleActionCancel, nil)
return sm
}
func main() {
sm := newExampleOrderStateMachine()
if err := sm.Check(); err != nil {
fmt.Println(err)
return
}
fsm.RegisterStateMachine(sm)
paidOrder := newExampleOrder()
receipt, _ := paidOrder.Pay(42, "card")
fmt.Println(receipt, paidOrder.CurrentState().Label())
_ = paidOrder.Refund(12)
fmt.Println(paidOrder.CurrentState().Label(), paidOrder.refundedAmount, paidOrder.remainingAmount())
_ = paidOrder.Refund(10)
fmt.Println(paidOrder.CurrentState().Label(), paidOrder.refundedAmount, paidOrder.remainingAmount())
fmt.Println(paidOrder.Pay(5, "card"))
_ = paidOrder.Cancel()
fmt.Println(paidOrder.CurrentState().Label(), paidOrder.refundedAmount, paidOrder.canceled)
unpaidOrder := newExampleOrder()
fmt.Println(unpaidOrder.Cancel())
_, _ = unpaidOrder.Pay(10, "cash")
_ = unpaidOrder.Cancel()
fmt.Println(unpaidOrder.CurrentState().Label(), unpaidOrder.refundedAmount, unpaidOrder.canceled)
}
Output: receipt:card:42 order.paid order.partial_refunded 12 30 order.partial_refunded 22 20 partially refunded order cannot be paid again order.canceled 42 true order.unpaid cannot cancel order.canceled 10 true
func NewStateMachine ¶
func NewStateMachine(name string) StateMachine
type StateMachineCheckError ¶ added in v0.10.1
type StateMachineCheckError struct {
MissingStateBuilders []StateLabel
UnreferencedStateBuilders []StateLabel
InvalidStateBuilders []StateBuilderError
}
StateMachineCheckError reports state-machine wiring problems found by Check.
func (*StateMachineCheckError) Error ¶ added in v0.10.1
func (err *StateMachineCheckError) Error() string
type StateMachineDefinitionError ¶ added in v0.10.1
type StateMachineDefinitionError struct {
Message string
}
StateMachineDefinitionError reports invalid state-machine setup input.
func (*StateMachineDefinitionError) Error ¶ added in v0.10.1
func (err *StateMachineDefinitionError) Error() string
type Transition ¶ added in v0.10.0
type Transition struct {
To StateLabel
Condition Condition
}
Transition defines a transition edge.
type TransitionError ¶
type TransitionError struct {
Action Action
Current StateLabel
}
TransitionError reports a missing transition for the current state/action.
func NewTransitionError ¶
func NewTransitionError(current StateLabel, action Action) *TransitionError
func (*TransitionError) Error ¶
func (te *TransitionError) Error() string