コアアイデア
ステートマシンには4つの構成要素があります:
| 概念 | 意味 |
|---|---|
| State | システムが置かれている状態(例: "CREATED", "PAID") |
| Event | 発生する何か(例: "PAY", "SHIP") |
| Action | 遷移が発生したときに実行されるもの |
| Transition | ルール: 状態Xから、イベントYを受け取ったら状態Zへ遷移(オプションで条件Wを満たす場合、アクションAを実行) |
SolonのStateMachine<State, Event, Payload>は、流れるようなDSLでこれらを結びつけます。
Hello World: ライトスイッチ
依存関係を追加します:
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-statemachine</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode
次に、OFFとONを切り替えるライトスイッチです:
import org.noear.solon.statemachine.EventContext;
import org.noear.solon.statemachine.StateMachine;
public class LightSwitchApp {
enum Event { PRESS }
enum State { OFF, ON }
public static void main(String[] args) {
StateMachine<State, Event, String> stateMachine = new StateMachine<>();
// OFF + PRESS -> ON
stateMachine.addTransition(t -> t.from(State.OFF).on(Event.PRESS).to(State.ON)
.then(context -> System.out.println("Light is ON")));
// ON + PRESS -> OFF
stateMachine.addTransition(t -> t.from(State.ON).on(Event.PRESS).to(State.OFF)
.then(context -> System.out.println("Light is OFF")));
// Test it
State currentState = State.OFF;
for (int i = 0; i < 6; i++) {
currentState = stateMachine.sendEvent(
Event.PRESS,
EventContext.of(currentState, null)
);
System.out.println("New state: " + currentState);
}
}
}
Enter fullscreen mode Exit fullscreen mode
実行すると、ライトは交互に切り替わります。sendEvent()は新しい状態を返します。状態変数を手動で管理する必要はありません。現在の状態を渡すだけで、マシンが次の状態を計算してくれます。
DSLの内訳
StateTransitionDeclには5つのメソッドがあります:
| メソッド | 必須 | 説明 |
|---|---|---|
.from(state) |
Yes | 元の状態 -- 複数指定可能 |
.to(state) |
Yes | 遷移先の状態(1つ) |
.on(event) |
Yes | トリガーとなるイベント |
.when(condition) |
No | オプションのガード条件 |
.then(action) |
No | オプションで実行されるアクション |
whenとthenは任意です。副作用なしのシンプルな状態遷移が必要な場合は次のようにします:
stateMachine.addTransition(t ->
t.from(State.DRAFT).to(State.PUBLISHED).on(Event.PUBLISH));
Enter fullscreen mode Exit fullscreen mode
実世界の例: 注文ステートマシン
注文ライフサイクルを、状態NONE -> CREATED -> PAID -> SHIPPED -> DELIVERED、および任意のキャンセル経路でモデル化してみましょう。
public enum OrderEvent { CREATE, PAY, SHIP, DELIVER, CANCEL }
public enum OrderState { NONE, CREATED, PAID, SHIPPED, DELIVERED, CANCELLED }
Enter fullscreen mode Exit fullscreen mode
EventContextを実装するOrderペイロードを定義します:
public class Order implements EventContext<OrderState, Order> {
private String id;
private String product;
private OrderState state;
private String status;
@Override
public OrderState getState() { return state; }
@Override
public Order getPayload() { return this; }
public void setState(OrderState state) { this.state = state; }
public void setStatus(String status) { this.status = status; }
}
Enter fullscreen mode Exit fullscreen mode
次にStateMachineを継承してステートマシンを構築します:
public class OrderStateMachine extends StateMachine<OrderState, OrderEvent, Order> {
public OrderStateMachine() {
from(OrderState.NONE).on(OrderEvent.CREATE).to(OrderState.CREATED).then(ctx -> {
Order payload = ctx.getPayload();
payload.setState(OrderState.CREATED);
payload.setStatus("Created");
});
from(OrderState.CREATED).on(OrderEvent.PAY).to(OrderState.PAID).then(ctx -> {
Order payload = ctx.getPayload();
payload.setState(OrderState.PAID);
payload.setStatus("Paid");
});
from(OrderState.PAID).on(OrderEvent.SHIP).to(OrderState.SHIPPED).then(ctx -> {
Order payload = ctx.getPayload();
payload.setState(OrderState.SHIPPED);
payload.setStatus("Shipped");
});
from(OrderState.SHIPPED).on(OrderEvent.DELIVER).to(OrderState.DELIVERED).then(ctx -> {
Order payload = ctx.getPayload();
payload.setState(OrderState.DELIVERED);
payload.setStatus("Delivered");
});
}
}
Enter fullscreen mode Exit fullscreen mode
テスト:
public class OrderTest {
public static void main(String[] args) {
OrderStateMachine stateMachine = new OrderStateMachine();
Order order = new Order("1", "iPhone 16 Pro Max", null);
stateMachine.sendEvent(OrderEvent.CREATE, order);
stateMachine.sendEvent(OrderEvent.PAY, order);
stateMachine.sendEvent(OrderEvent.SHIP, EventContext.of(order.getState(), order));
stateMachine.sendEvent(OrderEvent.DELIVER, EventContext.of(order.getState(), order));
}
}
Enter fullscreen mode Exit fullscreen mode
2つの使用スタイル
1. Lambdaスタイル(合成)
StateMachine<State, Event, Payload> sm = new StateMachine<>();
sm.addTransition(t -> t.from(X).on(E).to(Y).then(ctx -> { ... }));
Enter fullscreen mode Exit fullscreen mode
2. 継承スタイル
public class MyMachine extends StateMachine<State, Event, Payload> {
public MyMachine() {
from(X).on(E).to(Y).then(ctx -> { ... });
}
}
Enter fullscreen mode Exit fullscreen mode
どちらも同じ結果になります。複数の独立したマシンが必要な場合は合成を使い、マシンが自己完結したドメイン概念である場合は継承を使います。
いつ使うか(使わないか)
Solon State MachineはSolon Flowの補完として設計されており、置き換えではありません。
State Machineを使う場合:
- 離散的な状態とイベントがある場合(注文ライフサイクル、ドキュメントワークフロー、ゲームの状態)
- ネストされたif-elseやswitch文に代わる宣言的なアプローチが必要な場合
- 永続化が不要な場合 -- 状態はイベントコンテキストを通じてメモリ内で管理されます
Solon Flowを使う場合:
- 永続的で再開可能な長時間実行プロセスが必要な場合
- ビジネスユーザー向けのビジュアルデザイナーが必要な場合
- 複雑な分岐、並列実行、サブプロセスが必要な場合
2つは同じプロジェクト内で共存可能です。
率直な概要
永続化なし。 ステートマシンは設計上ステートレスです -- 現在の状態をEventContext経由で渡し、マシンは新しい状態を返します。保存は自分で管理します。
ビジュアルデザイナーなし。 Solon FlowのようなWebベースのデザイナーとは異なり、ステートマシンはコードのみです。
組み込みのイベントバス統合なし。 状態遷移は同期的に.then()アクションをトリガーします。
シンプルだが複雑なワークフローには不十分。 状態ロジックに並列状態やネストされたステートマシンが含まれる場合は、Solon Flowの方が適しています。
まとめ
| 項目 | 詳細 |
|---|---|
| 依存関係 |
org.noear:solon-statemachine (v3.4.3以降) |
| コアクラス | StateMachine<State, Event, Payload> |
| DSL | .from().on().to().when().then() |
| コンテキスト |
EventContext.of(state, payload) またはカスタム実装 |
| 永続化 | なし(呼び出し側が所有し、コンテキスト経由で渡す) |
| 適した用途 | 注文ライフサイクル、ドキュメントワークフロー、ゲームの状態、トグル |
この記事のすべての例は、公式ドキュメント solon.noear.org から引用しています。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.