easyFSM

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 15, 2023 License: MIT Imports: 3 Imported by: 0

README

Tutorial

I really like the concept of Domain-Driven Design (DDD), but I often struggle with handling state changes in domain objects.

I created this package with the motivation to simplify state management and use Mermaid for visualizing state transitions.

The usual way of managing state changes in domain objects often means dealing with a bunch of if conditions, and that can be error-prone.

This package aims to provide a more straightforward and organized way to manage states, making it easier for developers to understand and maintain their code.

By visualizing state transitions using Mermaid, users can gain a clearer understanding of the system's behavior and enhance their overall development experience.

Install

go get github.com/KScaesar/easyFSM

Example

Step 1: Define state-diagram on the Mermaid website
graph TD
    AwaitingPayment --> |Order.Placed| Confirmed
    Confirmed --> |Order.Shipped| Shipped
    Shipped --> |Order.Delivered| Delivered
    Confirmed --> |Order.Cancelled| Cancelled
    Shipped --> |Order.ReturnRequested| ReturnInProgress
    ReturnInProgress --> |Order.CargoReturned| Returned
    ReturnInProgress --> |Order.RefundRequested| RefundInProgress
    RefundInProgress --> |Order.Refunded| Refunded
    Returned --> |Order.Refunded| RefundInProgress
    Delivered --> |Order.ReturnRequested| ReturnInProgress
Step 2: Define transitions in Golang code
  1. The FSM should be placed in the global scope.
  2. When importing the package, transitions should be added using the DefineTransition function during the initialization step.
  3. The event triggering the transition.
// Parameters:
// - event: The event triggering the transition.
// - src: The source state from which the transition is allowed.
// - dest: The destination state to which the FSM will transition when the event occurs in the source state.
func (fsm FSM[E, S]) DefineTransition(event E, src, dest S) FSM[E, S]
// The FSM should be placed in the global scope.
var OrderFSM = NewFSM[OrderEvent, OrderState](OrderStateAwaitingPayment).
	DefineTransition(OrderEventPlaced, OrderStateAwaitingPayment, OrderStateConfirmed).
	DefineTransition(OrderEventShipped, OrderStateConfirmed, OrderStateShipped).
	DefineTransition(OrderEventDelivered, OrderStateShipped, OrderStateDelivered).
	DefineTransition(OrderEventCancelled, OrderStateConfirmed, OrderStateCancelled).
	DefineTransition(OrderEventReturnRequested, OrderStateShipped, OrderStateReturnInProgress).
	DefineTransition(OrderEventCargoReturned, OrderStateReturnInProgress, OrderStateReturned).
	DefineTransition(OrderEventRefundRequested, OrderStateReturnInProgress, OrderStateRefundInProgress).
	DefineTransition(OrderEventRefunded, OrderStateRefundInProgress, OrderStateRefunded).
	DefineTransition(OrderEventRefunded, OrderStateReturned, OrderStateRefundInProgress).
	DefineTransition(OrderEventReturnRequested, OrderStateDelivered, OrderStateReturnInProgress)

type OrderEvent string

const (
	OrderEventPlaced          OrderEvent = "Order.Placed"
	OrderEventShipped         OrderEvent = "Order.Shipped"
	OrderEventCancelled       OrderEvent = "Order.Cancelled"
	OrderEventDelivered       OrderEvent = "Order.Delivered"
	OrderEventReturnRequested OrderEvent = "Order.ReturnRequested"
	OrderEventCargoReturned   OrderEvent = "Order.CargoReturned"
	OrderEventRefundRequested OrderEvent = "Order.RefundRequested"
	OrderEventRefunded        OrderEvent = "Order.Refunded"
)

type OrderState string

const (
	OrderStateAwaitingPayment  OrderState = "AwaitingPayment"  // 訂單已建立,但尚未收到付款
	OrderStateConfirmed        OrderState = "Confirmed"        // 訂單已經確認,支付和庫存等相關事宜已完成,等待商品出貨
	OrderStateShipped          OrderState = "Shipped"          // 商品已經發貨,正在運送途中
	OrderStateDelivered        OrderState = "Delivered"        // 商品已經成功送達到顧客手中,交易完成
	OrderStateCancelled        OrderState = "Cancelled"        // 訂單在處理過程中被取消,交易不會繼續進行
	OrderStateReturnInProgress OrderState = "ReturnInProgress" // 顧客申請退貨,退貨正在處理中
	OrderStateReturned         OrderState = "Returned"         // 退貨流程已完成,商品已經退回並接收
	OrderStateRefundInProgress OrderState = "RefundInProgress" // 退款正在處理中,將退還付款給顧客
	OrderStateRefunded         OrderState = "Refunded"         // 退款已經完成,付款已退還給顧客
	// OrderStateError            OrderState = "Error"            // 訂單面臨付款錯誤、庫存問題或其他技術問題
)
Step 3: Verify that Golang FSM meets expectations
func TestMermaidGraphByTopDown(t *testing.T) {
	expected := `
graph TD
  AwaitingPayment --> |Order.Placed| Confirmed
  Confirmed --> |Order.Shipped| Shipped
  Shipped --> |Order.Delivered| Delivered
  Confirmed --> |Order.Cancelled| Cancelled
  Shipped --> |Order.ReturnRequested| ReturnInProgress
  ReturnInProgress --> |Order.CargoReturned| Returned
  ReturnInProgress --> |Order.RefundRequested| RefundInProgress
  RefundInProgress --> |Order.Refunded| Refunded
  Returned --> |Order.Refunded| RefundInProgress
  Delivered --> |Order.ReturnRequested| ReturnInProgress
`
	actual := MermaidGraphByTopDown(OrderFSM, nil)

	if expected != actual {
		t.Errorf("expected = %v, but actual = %v", expected, actual)
	}
}
Step 4: Call the domain object method in Domain-Driven Design (DDD)

playground

type Order struct {
	Id string
	// ... other field
	State OrderState
}

func (o *Order) ReturnRequest() error {
	fsm := OrderStateFSM.CopyFSM(o.State) // copy by value

	return fsm.OnAction(OrderEventReturnRequested, func(nextState OrderState) error {
		o.State = nextState
		fmt.Println(o.State)
		return nil
	})
}

func OrderUseCaseSuccess(repo OrderRepository, ctx context.Context) error {
	order, err := repo.LockOrderById(ctx, "order_state_is_Delivered")
	if err != nil {
		return fmt.Errorf("get obj from db: %w", err)
	}

	// ReturnInProgress
	return order.ReturnRequest()
}

func OrderUseCaseFail(repo OrderRepository, ctx context.Context) error {
	order, err := repo.LockOrderById(ctx, "order_state_is_Confirmed")
	if err != nil {
		return fmt.Errorf("get obj from db: %w", err)
	}

	// key = {event: Order.ReturnRequested, requiredState: Delivered}, but currentState = Confirmed: state not match
	return order.ReturnRequest()
}

type OrderRepository interface {
	LockOrderById(ctx context.Context, oId string) (Order, error)
}

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrStateNotMatch   = fmt.Errorf("state not match")
	ErrEventNotDefined = fmt.Errorf("event not defined")
)

Functions

func MermaidGraphByTopDown

func MermaidGraphByTopDown[E, S constraints.Ordered](fsm FSM[E, S], transform TextTransformFunc) string

MermaidGraphByTopDown generates a Mermaid graph representation of the FSM. It takes an FSM object and a TextTransformFunc as input. The TextTransformFunc is an optional function that can be used to transform the text representation of states and events before including them in the graph.

The function returns a string containing the Mermaid graph definition.

The Mermaid graph is generated in the Top-Down (TD) style, where each transition from a source state to a destination state is represented with an arrow labeled by the corresponding event.

Example Usage:

fsm := NewFSM(StateA)
fsm.AddTransition(StateA, EventX, StateB)
fsm.AddTransition(StateB, EventY, StateC)
graph := MermaidGraphByTopDown(fsm, nil)
fmt.Println(graph)

Output:

graph TD
  StateA --> |EventX| StateB
  StateB --> |EventY| StateC

Types

type FSM

type FSM[E, S constraints.Ordered] struct {
	// contains filtered or unexported fields
}

func NewFSM

func NewFSM[E, S constraints.Ordered](startState S) FSM[E, S]

func (FSM[E, S]) CopyFSM

func (fsm FSM[E, S]) CopyFSM(currentState S) FSM[E, S]

func (FSM[E, S]) CurrentState added in v0.1.8

func (fsm FSM[E, S]) CurrentState() S

func (FSM[E, S]) DefineTransition

func (fsm FSM[E, S]) DefineTransition(event E, src, dest S) FSM[E, S]

DefineTransition adds a new transition to the Finite State Machine (FSM). It defines that when a specific event occurs in a particular source state, the FSM will transition to the destination state. If the same event and source state combination already exists in the FSM, it will panic with an error indicating that a duplicated transition is being added.

src -->|event| dest

Parameters: - event: The event triggering the transition. - src: The source state from which the transition is allowed. - dest: The destination state to which the FSM will transition when the event occurs in the source state.

Note: 1. The FSM should be placed in the global scope. 2. When importing the package, transitions should be added using the DefineTransition function during the initialization step.

func (FSM[E, S]) OnAction

func (fsm FSM[E, S]) OnAction(event E, action func(nextState S) error) error

OnAction triggers the transition in the Finite State Machine (FSM) when a specific event occurs. It first calls the internal doTransition function to determine the destination state after the event. If the transition is successful, it calls the provided action function with the destination state as a parameter. The action function is responsible for performing any necessary actions or operations associated with the state transition.

Parameters: - event: The event that triggers the transition. - action: A function that takes the destination state as a parameter and returns an error if any.

Example
package main

import (
	"context"
	"fmt"

	"github.com/KScaesar/easyFSM"
)

type OrderEvent string

const (
	OrderEventPlaced          OrderEvent = "Order.Placed"
	OrderEventShipped         OrderEvent = "Order.Shipped"
	OrderEventCancelled       OrderEvent = "Order.Cancelled"
	OrderEventDelivered       OrderEvent = "Order.Delivered"
	OrderEventReturnRequested OrderEvent = "Order.ReturnRequested"
	OrderEventCargoReturned   OrderEvent = "Order.CargoReturned"
	OrderEventRefundRequested OrderEvent = "Order.RefundRequested"
	OrderEventRefunded        OrderEvent = "Order.Refunded"
)

type OrderState string

const (
	OrderStateAwaitingPayment  OrderState = "AwaitingPayment"  // 訂單已建立,但尚未收到付款
	OrderStateConfirmed        OrderState = "Confirmed"        // 訂單已經確認,支付和庫存等相關事宜已完成,等待商品出貨
	OrderStateShipped          OrderState = "Shipped"          // 商品已經發貨,正在運送途中
	OrderStateDelivered        OrderState = "Delivered"        // 商品已經成功送達到顧客手中,交易完成
	OrderStateCancelled        OrderState = "Cancelled"        // 訂單在處理過程中被取消,交易不會繼續進行
	OrderStateReturnInProgress OrderState = "ReturnInProgress" // 顧客申請退貨,退貨正在處理中
	OrderStateReturned         OrderState = "Returned"         // 退貨流程已完成,商品已經退回並接收
	OrderStateRefundInProgress OrderState = "RefundInProgress" // 退款正在處理中,將退還付款給顧客
	OrderStateRefunded         OrderState = "Refunded"         // 退款已經完成,付款已退還給顧客
	OrderStateError            OrderState = "Error"            // 訂單面臨付款錯誤、庫存問題或其他技術問題
)

// The FSM should be placed in the global scope.
var OrderFSM = easyFSM.NewFSM[OrderEvent, OrderState](OrderStateAwaitingPayment).
	DefineTransition(OrderEventPlaced, OrderStateAwaitingPayment, OrderStateConfirmed).
	DefineTransition(OrderEventShipped, OrderStateConfirmed, OrderStateShipped).
	DefineTransition(OrderEventDelivered, OrderStateShipped, OrderStateDelivered).
	DefineTransition(OrderEventCancelled, OrderStateConfirmed, OrderStateCancelled).
	DefineTransition(OrderEventReturnRequested, OrderStateShipped, OrderStateReturnInProgress).
	DefineTransition(OrderEventCargoReturned, OrderStateReturnInProgress, OrderStateReturned).
	DefineTransition(OrderEventRefundRequested, OrderStateReturnInProgress, OrderStateRefundInProgress).
	DefineTransition(OrderEventRefunded, OrderStateRefundInProgress, OrderStateRefunded).
	DefineTransition(OrderEventRefunded, OrderStateReturned, OrderStateRefundInProgress).
	DefineTransition(OrderEventReturnRequested, OrderStateDelivered, OrderStateReturnInProgress)

func main() {
	repo := MemoryOrderRepository{}
	ctx := context.Background()

	fmt.Printf("UseCaseSuccess:\n")
	err := OrderUseCaseSuccess(repo, ctx)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Printf("\nUseCaseFail:\n")
	err = OrderUseCaseFail(repo, ctx)
	if err != nil {
		fmt.Println(err)
	}
}

type Order struct {
	Id string
	// ... other field
	State OrderState
}

func (o *Order) ReturnRequest() error {
	fsm := OrderFSM.CopyFSM(o.State) // copy by value

	return fsm.OnAction(OrderEventReturnRequested, func(nextState OrderState) error {
		o.State = nextState
		fmt.Println(o.State)
		return nil
	})
}

func OrderUseCaseSuccess(repo OrderRepository, ctx context.Context) error {
	order, err := repo.LockOrderById(ctx, "order_state_is_Delivered")
	if err != nil {
		return fmt.Errorf("get obj from db: %w", err)
	}

	// ReturnInProgress
	return order.ReturnRequest()
}

func OrderUseCaseFail(repo OrderRepository, ctx context.Context) error {
	order, err := repo.LockOrderById(ctx, "order_state_is_Confirmed")
	if err != nil {
		return fmt.Errorf("get obj from db: %w", err)
	}

	// key = {event: Order.ReturnRequested, requiredState: Delivered}, but currentState = Confirmed: state not match
	return order.ReturnRequest()
}

type OrderRepository interface {
	LockOrderById(ctx context.Context, oId string) (Order, error)
}

type MemoryOrderRepository struct{}

func (MemoryOrderRepository) LockOrderById(_ context.Context, oId string) (Order, error) {
	store := map[string]Order{
		"order_state_is_Delivered": {
			Id:    "order_state_is_Delivered",
			State: OrderStateDelivered,
		},
		"order_state_is_Confirmed": {
			Id:    "order_state_is_Confirmed",
			State: OrderStateConfirmed,
		},
	}
	return store[oId], nil
}
Output:

func (FSM[E, S]) ShowStates added in v0.2.0

func (fsm FSM[E, S]) ShowStates() []S

type TextTransformFunc

type TextTransformFunc func(text string) string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL