Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,4 +450,4 @@ var cachedRemoteProxy = Proxy<int, string>.Create(id => remoteProxy.Execute(id))
| -------------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Creational** | [Factory](docs/patterns/creational/factory/factory.md) ✓ • [Composer](docs/patterns/creational/builder/composer.md) ✓ • [ChainBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [BranchBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [MutableBuilder](docs/patterns/creational/builder/mutablebuilder.md) ✓ • [Prototype](docs/patterns/creational/prototype/prototype.md) ✓ • [Singleton](docs/patterns/creational/singleton/singleton.md) ✓ |
| **Structural** | [Adapter](docs/patterns/structural/adapter/fluent-adapter.md) ✓ • [Bridge](docs/patterns/structural/bridge/bridge.md) ✓ • [Composite](docs/patterns/structural/composite/composite.md) ✓ • [Decorator](docs/patterns/structural/decorator/decorator.md) ✓ • [Facade](docs/patterns/structural/facade/facade.md) ✓ • [Flyweight](docs/patterns/structural/flyweight/index.md) ✓ • [Proxy](docs/patterns/structural/proxy/index.md) ✓ |
| **Behavioral** | [Strategy](docs/patterns/behavioral/strategy/strategy.md) ✓ • [TryStrategy](docs/patterns/behavioral/strategy/trystrategy.md) ✓ • [ActionStrategy](docs/patterns/behavioral/strategy/actionstrategy.md) ✓ • [ActionChain](docs/patterns/behavioral/chain/actionchain.md) ✓ • [ResultChain](docs/patterns/behavioral/chain/resultchain.md) ✓ • [ReplayableSequence](docs/patterns/behavioral/iterator/replayablesequence.md) ✓ • [WindowSequence](docs/patterns/behavioral/iterator/windowsequence.md) ✓ • [Command](docs/patterns/behavioral/command/command.md) ✓ • [Mediator](docs/patterns/behavioral/mediator/mediator.md) ✓ • [Memento](docs/patterns/behavioral/memento/memento.md) ✓ • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) |
| **Behavioral** | [Strategy](docs/patterns/behavioral/strategy/strategy.md) ✓ • [TryStrategy](docs/patterns/behavioral/strategy/trystrategy.md) ✓ • [ActionStrategy](docs/patterns/behavioral/strategy/actionstrategy.md) ✓ • [ActionChain](docs/patterns/behavioral/chain/actionchain.md) ✓ • [ResultChain](docs/patterns/behavioral/chain/resultchain.md) ✓ • [ReplayableSequence](docs/patterns/behavioral/iterator/replayablesequence.md) ✓ • [WindowSequence](docs/patterns/behavioral/iterator/windowsequence.md) ✓ • [Command](docs/patterns/behavioral/command/command.md) ✓ • [Mediator](docs/patterns/behavioral/mediator/mediator.md) ✓ • [Memento](docs/patterns/behavioral/memento/memento.md) ✓ • [Observer](docs/patterns/behavioral/observer/observer.md) ✓ • [State](docs/patterns/behavioral/state/state.md) ✓ • Template Method (planned) • Visitor (planned) |
130 changes: 130 additions & 0 deletions docs/examples/async-state-machine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Async State Machine — Connection Lifecycle

A small, production‑shaped demo of AsyncStateMachine managing a connection lifecycle. It shows async entry/exit hooks, async effects, and default stay behavior.

What you’ll see
- Declarative states and transitions using When → Permit/Stay → Do.
- Exit → Effect → Enter order on cross‑state transitions.
- Default Stay that ignores unknown events while connected.

Code (excerpt)
```csharp
using PatternKit.Behavioral.State;

public static class ConnectionStateDemo
{
public enum Mode { Disconnected, Connecting, Connected, Error }
public readonly record struct NetEvent(string Kind);

public static async ValueTask<(Mode Final, List<string> Log)> RunAsync(params string[] events)
{
var log = new List<string>();
var m = AsyncStateMachine<Mode, NetEvent>.Create()
.InState(Mode.Disconnected, s => s
.OnExit(async (e, ct) => { await Task.Yield(); log.Add("exit:Disconnected"); })
.When((e, ct) => new ValueTask<bool>(e.Kind == "connect")).Permit(Mode.Connecting).Do(async (e, ct) => { await Task.Delay(1, ct); log.Add("effect:dial"); })
)
.InState(Mode.Connecting, s => s
.OnEnter(async (e, ct) => { await Task.Yield(); log.Add("enter:Connecting"); })
.OnExit(async (e, ct) => { await Task.Yield(); log.Add("exit:Connecting"); })
.When((e, ct) => new ValueTask<bool>(e.Kind == "ok")).Permit(Mode.Connected).Do(async (e, ct) => { await Task.Yield(); log.Add("effect:handshake"); })
.When((e, ct) => new ValueTask<bool>(e.Kind == "fail")).Permit(Mode.Error).Do(async (e, ct) => { await Task.Yield(); log.Add("effect:cleanup"); })
)
.InState(Mode.Connected, s => s
.OnEnter(async (e, ct) => { await Task.Yield(); log.Add("enter:Connected"); })
.Otherwise().Stay().Do(async (e, ct) => { await Task.Yield(); log.Add("effect:noop"); })
)
.Build();

var state = Mode.Disconnected;
foreach (var k in events)
{
var (_, next) = await m.TryTransitionAsync(state, new NetEvent(k));
state = next;
}
return (state, log);
}
}
```

How to run
```bat
rem Build everything (Debug)
dotnet build PatternKit.slnx -c Debug

rem Run tests (includes this demo)
dotnet test PatternKit.slnx -c Debug --filter FullyQualifiedName~AsyncStateDemo
```

Relevant files
- src/PatternKit.Examples/AsyncStateDemo/AsyncStateDemo.cs
- test/PatternKit.Examples.Tests/AsyncStateDemo/AsyncStateDemoTests.cs
- docs/patterns/behavioral/state/state.md — pattern reference (sync + async)
# State Machine (Sync and Async)

A fluent, allocation‑light state machine with entry/exit hooks, optional default transitions, and a predictable first‑match rule order. Ships in two forms:
- StateMachine<TState, TEvent> — synchronous delegates
- AsyncStateMachine<TState, TEvent> — async predicates/effects/hooks

Design notes
- Immutable after Build(); share safely across threads.
- You hold the current state; pass it to the machine per call.
- First matching When rule in registration order wins; optional per‑state Otherwise default.
- Exit → Effect → Enter ordering for cross‑state transitions; Stay executes effect only.

Quick start (sync)
Comment thread
JerrettDavis marked this conversation as resolved.
```csharp
var m = StateMachine<OrderState, OrderEvent>.Create()
.InState(OrderState.New, s => s
.OnExit((in OrderEvent _) => log.Add("audit:new->"))
.When(static (in OrderEvent e) => e.Kind == "pay").Permit(OrderState.Paid).Do((in OrderEvent _) => log.Add("charge"))
.When(static (in OrderEvent e) => e.Kind == "cancel").Permit(OrderState.Cancelled).Do((in OrderEvent _) => log.Add("cancel"))
)
.InState(OrderState.Paid, s => s
.OnEnter((in OrderEvent _) => log.Add("notify:paid"))
.When(static (in OrderEvent e) => e.Kind == "ship").Permit(OrderState.Shipped).Do((in OrderEvent _) => log.Add("ship"))
)
.Build();

var state = OrderState.New;
m.TryTransition(ref state, in new OrderEvent("pay"));
```

Quick start (async)
```csharp
var m = AsyncStateMachine<State, Ev>.Create()
.InState(State.Idle, s => s
.OnExit(async (e, ct) => { await Task.Yield(); log.Add("exit:Idle"); })
.When((e, ct) => new ValueTask<bool>(e.Kind == "go")).Permit(State.Active).Do(async (e, ct) => { await Task.Delay(1, ct); log.Add("effect:go"); })
)
.InState(State.Active, s => s
.OnEnter(async (e, ct) => { await Task.Yield(); log.Add("enter:Active"); })
)
.Build();

var (handled, next) = await m.TryTransitionAsync(State.Idle, new Ev("go"));
```

Semantics
- Cross‑state: exit hooks of current, then effect, then entry hooks of next.
- Stay/internal: effect only; entry/exit hooks do not run.
- Default per state: .Otherwise().Permit(...) or .Otherwise().Stay() when no predicate matches.
- Comparer: supply a custom equality comparer via .Comparer(...) in the builder when needed.

API summary
- StateMachine<TState, TEvent>
- bool TryTransition(ref TState state, in TEvent @event)
- void Transition(ref TState state, in TEvent @event)
- AsyncStateMachine<TState, TEvent>
- ValueTask<(bool handled, TState state)> TryTransitionAsync(TState state, TEvent @event, CancellationToken ct = default)
- ValueTask<TState> TransitionAsync(TState state, TEvent @event, CancellationToken ct = default)

Examples
- docs/examples/state-machine.md — order lifecycle (sync)
- docs/examples/async-state-machine.md — connection lifecycle (async)

Tips
- Prefer enums or interned strings for TState; or pass a comparer.
- Keep predicates cheap; effects are a good place for side‑effects.
- Async flavor avoids ref parameters by returning the updated state.

4 changes: 4 additions & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ Welcome! This section collects small, focused demos that show **how to compose b
* **Document Editing History (Memento)**
A simple document buffer with tagged snapshots, undo/redo, jump-to-version, and duplicate suppression illustrating the **Memento** pattern's practical shape in a UI/editor workflow.

* **State Machine — Order Lifecycle**
A fluent state machine driving an order lifecycle with entry/exit hooks, transition effects, and default per‑state behavior. Shows determinism (first‑match wins), internal (Stay) vs cross‑state transitions, and log/audit side‑effects.

## How to run

From the repo root:
Expand Down Expand Up @@ -81,6 +84,7 @@ dotnet test PatternKit.slnx -c Release
* **Flyweight Glyph Cache:** `FlyweightDemo` (+ `FlyweightDemoTests`) — glyph width layout & style sharing.
* **Flyweight Structural Tests:** `Structural/Flyweight/FlyweightTests.cs` — preload, concurrency, comparer, guards.
* **Memento Document Demo:** `MementoDemo` — buffer edits with undo/redo and tags.
* **State Machine Demo:** `OrderStateDemo` (+ `StateDemoTests`) — order lifecycle with entry/exit hooks and default behaviors.
* **Tests:** `PatternKit.Examples.Tests/*` use TinyBDD scenarios that read like specs.

## Why these demos exist
Expand Down
92 changes: 92 additions & 0 deletions docs/examples/state-machine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# State Machine — Order Lifecycle

A small, production‑shaped demo of a fluent State Machine managing an order’s lifecycle. It shows entry/exit hooks, transition effects, and default per‑state behavior.

What you’ll see
- Declarative states and transitions using When → Permit/Stay → Do.
- Entry/exit hooks for notifications and audits.
- Default stay transitions that “ignore” unknown events in terminal states.
- Immutable machine shared across calls; you pass state by ref.

---
## Code
```csharp
using PatternKit.Behavioral.State;

public static class OrderStateDemo
{
public enum OrderState { New, Paid, Shipped, Delivered, Cancelled, Refunded }
public readonly record struct OrderEvent(string Kind);

public static (OrderState Final, List<string> Log) Run(params string[] events)
{
var log = new List<string>();
var machine = StateMachine<OrderState, OrderEvent>.Create()
.InState(OrderState.New, s => s
.OnExit((in OrderEvent _) => log.Add("audit:new->"))
.When(static (in OrderEvent e) => e.Kind == "pay").Permit(OrderState.Paid).Do((in OrderEvent _) => log.Add("charge"))
.When(static (in OrderEvent e) => e.Kind == "cancel").Permit(OrderState.Cancelled).Do((in OrderEvent _) => log.Add("cancel"))
)
.InState(OrderState.Paid, s => s
.OnEnter((in OrderEvent _) => log.Add("notify:paid"))
.OnExit((in OrderEvent _) => log.Add("audit:paid->"))
.When(static (in OrderEvent e) => e.Kind == "ship").Permit(OrderState.Shipped).Do((in OrderEvent _) => log.Add("ship"))
)
.InState(OrderState.Shipped, s => s
.OnEnter((in OrderEvent _) => log.Add("notify:shipped"))
.OnExit((in OrderEvent _) => log.Add("audit:shipped->"))
.When(static (in OrderEvent e) => e.Kind == "deliver").Permit(OrderState.Delivered).Do((in OrderEvent _) => log.Add("deliver"))
)
.InState(OrderState.Delivered, s => s
.OnEnter((in OrderEvent _) => log.Add("notify:delivered"))
.Otherwise().Stay().Do((in OrderEvent _) => log.Add("ignore"))
)
.InState(OrderState.Cancelled, s => s
.OnEnter((in OrderEvent _) => log.Add("notify:cancelled"))
.When(static (in OrderEvent e) => e.Kind == "refund").Permit(OrderState.Refunded).Do((in OrderEvent _) => log.Add("refund"))
.Otherwise().Stay().Do((in OrderEvent _) => log.Add("ignore"))
)
.InState(OrderState.Refunded, s => s
.OnEnter((in OrderEvent _) => log.Add("notify:refunded"))
.Otherwise().Stay().Do((in OrderEvent _) => log.Add("ignore"))
)
.Build();

var state = OrderState.New;
foreach (var k in events)
machine.TryTransition(ref state, in new OrderEvent(k));

return (state, log);
}
}
```

---
## Behavior
- pay → ship → deliver takes you New → Paid → Shipped → Delivered.
- cancel → refund takes you New → Cancelled → Refunded.
- Delivered ignores further events using a default Stay() rule.

---
## How to run
From the repo root:

```bash
rem Build everything (Release)
dotnet build PatternKit.slnx -c Release

rem Run tests for examples (includes this demo)
dotnet test PatternKit.slnx -c Release --filter FullyQualifiedName~StateDemo
```

Relevant files
- src/PatternKit.Examples/StateDemo/StateDemo.cs — the demo machine.
- test/PatternKit.Examples.Tests/StateDemo/StateDemoTests.cs — specs for paths and logs.
- docs/patterns/behavioral/state/state.md — the pattern reference.

---
## Tips
- Entry/exit hooks run on cross‑state transitions only (not Stay()).
- Effects run between exit and entry; useful for audit/side‑effects.
- Prefer enums or interned strings for TState when identity matters; you can also supply a custom comparer via .Comparer(...).

3 changes: 3 additions & 0 deletions docs/examples/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@

- name: Reactive Transaction (Dynamic Discounts, Tax, Total)
href: reactive-transaction.md

- name: Async Connection State Machine
href: async-state-machine.md
Empty file.
Loading
Loading