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
6 changes: 6 additions & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ Welcome! This section collects small, focused demos that show **how to compose b
* **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.

* **Template Method Data Processor**
Shows how to use the Template Method pattern to define a reusable, extensible workflow for data processing. Demonstrates hooks for pre/post processing, thread-safety, and easy extensibility. See [Template Method Demo](template-method-demo.md).

* **Template Method Async Pipeline**
End-to-end async pipeline (fetch → transform → store) with cancellation, optional synchronization, and error observation. Shows both subclassing (`AsyncTemplateMethod`) and fluent (`AsyncTemplate`) approaches. See [Template Method Async Demo](template-method-async-demo.md).

## How to run

From the repo root:
Expand Down
68 changes: 68 additions & 0 deletions docs/examples/template-method-async-demo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Template Method Async Demo

This demo shows non-trivial, end-to-end async workflows with PatternKit’s async Template variants, including cancellation, concurrency control, and error observation.

## Async subclassing demo: AsyncDataPipeline
A 3-stage pipeline (fetch → transform → store) with optional serialization and cancellation.

```csharp
public sealed class AsyncDataPipeline : AsyncTemplateMethod<int, string>
{
protected override bool Synchronized => false; // enable only when shared mutable state requires it

protected override async ValueTask OnBeforeAsync(int id, CancellationToken ct)
{
Console.WriteLine($"[BeforeAsync] {id}");
await Task.Yield();
}

protected override async ValueTask<string> StepAsync(int id, CancellationToken ct)
{
await Task.Delay(25, ct); // fetch
await Task.Delay(10, ct); // transform
await Task.Delay(5, ct); // store
return $"VAL-{id}";
}

protected override ValueTask OnAfterAsync(int id, string result, CancellationToken ct)
{
Console.WriteLine($"[AfterAsync] {id} -> {result}");
return default;
}
}

var pipe = new AsyncDataPipeline();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
var outVal = await pipe.ExecuteAsync(42, cts.Token);
```

## Async fluent demo: TemplateAsyncFluentDemo
Same shape using the fluent builder with multiple hooks and error handling.

```csharp
var tpl = AsyncTemplate<int, string>
.Create(async (id, ct) =>
{
await Task.Delay(15, ct);
if (id < 0) throw new InvalidOperationException("invalid id");
return $"VAL-{id}";
})
.Before((id, ct) => { Console.WriteLine($"[BeforeAsync] {id}"); return default; })
.After((id, res, ct) => { Console.WriteLine($"[AfterAsync] {id} -> {res}"); return default; })
.OnError((id, err, ct) => { Console.WriteLine($"[ErrorAsync] {id}: {err}"); return default; })
.Synchronized(false)
.Build();

var (ok, res, err) = await tpl.TryExecuteAsync(42);
Console.WriteLine(ok ? $"OK: {res}" : $"ERR: {err}");
```

## Guidance
- Prefer async for I/O-bound steps or where cancellation must be respected.
- Use `.Synchronized()` sparingly; it introduces a critical section. Keep steps idempotent and fast.
- Use `TryExecuteAsync` to keep control flow non-throwing and centralize error observation.

## See Also
- [Template Method Pattern](../patterns/behavioral/template/templatemethod.md)
- Synchronous demo: [Template Method Demo](template-method-demo.md)

66 changes: 66 additions & 0 deletions docs/examples/template-method-demo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Template Method Demo

This demo shows two ways to use PatternKit’s Template Method:

- Subclassing: derive from `TemplateMethod<TContext, TResult>` and override hooks.
- Fluent: compose a `Template<TContext, TResult>` with `Before/After/OnError/Synchronized`.

Both give you a consistent workflow shape with customizable steps and optional synchronization.

## Subclassing demo (DataProcessor)
Counts words in a string while logging before/after.

```csharp
public class DataProcessor : TemplateMethod<string, int>
{
protected override void OnBefore(string context)
{
Console.WriteLine($"Preparing to process: {context}");
}

protected override int Step(string context)
{
return context.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}

protected override void OnAfter(string context, int result)
{
Console.WriteLine($"Processed '{context}' with result: {result}");
}
}

var processor = new DataProcessor();
var result = processor.Execute("The quick brown fox jumps over the lazy dog");
Console.WriteLine($"Word count: {result}");
```

## Fluent demo (TemplateFluentDemo)
Same behavior using the fluent builder, plus non-throwing `TryExecute` and an `OnError` hook.

```csharp
var tpl = Template<string, int>
.Create(ctx => ctx.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length)
.Before(ctx => Console.WriteLine($"[Before] Input: '{ctx}'"))
.After((ctx, res) => Console.WriteLine($"[After] '{ctx}' -> words: {res}"))
.OnError((ctx, err) => Console.WriteLine($"[Error] Input '{ctx}', error: {err}"))
.Synchronized() // optional
.Build();

if (tpl.TryExecute("The quick brown fox", out var count, out var error))
Console.WriteLine($"Word count: {count}");
else
Console.WriteLine($"Failed: {error}");
```

## When to use which
- Prefer subclassing when the algorithm is a stable concept in your domain, and you want a strongly‑named type.
- Prefer fluent when you want to compose quickly, add multiple hooks, or opt into `TryExecute` easily.

## Thread safety
- Subclassing: override `Synchronized` to serialize `Execute` calls.
- Fluent: call `.Synchronized()` on the builder.
- Leave off for maximum concurrency when your step/hook logic is already thread-safe.

## See Also
- [Template Method Pattern Documentation](../patterns/behavioral/template/templatemethod.md)
- Refactoring Guru: Template Method — https://refactoring.guru/design-patterns/template-method
6 changes: 6 additions & 0 deletions docs/examples/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,9 @@

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

- name: Template Method (Subclassing)
href: template-method-demo.md

- name: Template Method (Async)
href: template-method-async-demo.md
106 changes: 106 additions & 0 deletions docs/patterns/behavioral/template/asynctemplate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# AsyncTemplate<TContext, TResult>

A fluent, allocation-light async Template: define a fixed workflow (before → step → after), add async/sync hooks, opt into synchronization, and choose throwing or non-throwing execution.

---

## What it is

- Async skeleton with three phases: Before (0..n), Step (1), After (0..n)
- Non-throwing path via `TryExecuteAsync(context)` returning `(ok, result, error)`
- Optional per-instance synchronization via `SemaphoreSlim`
- Immutable and thread-safe after `Build()`

---

## TL;DR

```csharp
using PatternKit.Behavioral.Template;

var tpl = AsyncTemplate<int, string>
.Create(async (id, ct) =>
{
await Task.Delay(15, ct);
if (id < 0) throw new InvalidOperationException("invalid id");
return $"VAL-{id}";
})
.Before((id, ct) => { Console.WriteLine($"[BeforeAsync] {id}"); return default; })
.After((id, res, ct) => { Console.WriteLine($"[AfterAsync] {id} -> {res}"); return default; })
.OnError((id, err, ct) => { Console.WriteLine($"[ErrorAsync] {id}: {err}"); return default; })
.Synchronized() // optional
.Build();

var (ok, result, error) = await tpl.TryExecuteAsync(42);
Console.WriteLine(ok ? $"OK: {result}" : $"ERR: {error}");
```

---

## API shape

```csharp
var tpl = AsyncTemplate<TContext, TResult>
.Create(static (TContext ctx, CancellationToken ct) => /* ValueTask<TResult> */)
.Before(static (TContext ctx, CancellationToken ct) => /* ValueTask */) // 0..n (async)
.Before(static (TContext ctx) => { /* side-effect */ }) // 0..n (sync overload)
.After(static (TContext ctx, TResult res, CancellationToken ct) => /* ValueTask */) // 0..n (async)
.After(static (TContext ctx, TResult res) => { /* side-effect */ }) // 0..n (sync overload)
.OnError(static (TContext ctx, string error, CancellationToken ct) => /* ValueTask */) // 0..n (async)
.OnError(static (TContext ctx, string error) => { /* observe */ }) // 0..n (sync overload)
.Synchronized() // optional
.Build();

// Throws on failure
TResult result = await tpl.ExecuteAsync(context, ct);

// Non-throwing; returns tuple (ok, result?, error?)
(bool ok, TResult? result, string? error) = await tpl.TryExecuteAsync(context, ct);
```

Notes
- Multiple hooks compose; registration order is invocation order.
- OnError hooks run only when TryExecuteAsync catches an exception.
- Synchronized() uses an async mutex; keep the critical section small.

---

## Testing (TinyBDD-style)

```csharp
using PatternKit.Behavioral.Template;
using TinyBDD;
using TinyBDD.Xunit;

var tpl = AsyncTemplate<string, int>
.Create(async (ctx, ct) => { await Task.Yield(); return ctx.Length; })
.Before((ctx, ct) => { Console.WriteLine($"before:{ctx}"); return default; })
.After((ctx, res, ct) => { Console.WriteLine($"after:{ctx}:{res}"); return default; })
.Build();

var r = await tpl.ExecuteAsync("abc"); // 3
```

---

## Design notes

- No reflection/LINQ in the hot path; simple async delegate calls and an optional async lock.
- Immutable after Build() so instances can be safely shared across threads.
- Sync and async hooks both supported; they are adapted internally to async.

---

## Gotchas

- ExecuteAsync throws; OnError hooks are not invoked by ExecuteAsync.
- TryExecuteAsync captures ex.Message as error; result is default when failing.
- Synchronized serializes executions; prefer idempotent, short steps.

---

## See also

- Subclassing: [TemplateMethod<TContext, TResult>](./templatemethod.md)
- Synchronous fluent: [Template<TContext, TResult>](./template.md)
- Demos: [Template Method Demo](../../../examples/template-method-demo.md), [Template Method Async Demo](../../../examples/template-method-async-demo.md)
101 changes: 101 additions & 0 deletions docs/patterns/behavioral/template/template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Template<TContext, TResult>

A fluent, allocation-light Template: define a fixed workflow (before → step → after), add optional hooks, opt into synchronization, and choose throwing or non-throwing execution.

---

## What it is

- Skeleton with three phases: Before (0..n), Step (1), After (0..n)
- Non-throwing path via TryExecute(context, out result, out error)
- Optional per-instance synchronization (mutual exclusion)
- Immutable and thread-safe after Build()

---

## TL;DR

```csharp
using PatternKit.Behavioral.Template;

var tpl = Template<string, int>
.Create(ctx => ctx.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length)
.Before(ctx => Console.WriteLine($"[Before] '{ctx}'"))
.After((ctx, res) => Console.WriteLine($"[After] '{ctx}' -> {res}"))
.OnError((ctx, err) => Console.WriteLine($"[Error] '{ctx}': {err}"))
.Synchronized() // optional
.Build();

var ok = tpl.TryExecute("The quick brown fox", out var words, out var error);
Console.WriteLine(ok ? $"Words={words}" : $"Failed: {error}");
```

---

## API shape

```csharp
var tpl = Template<TContext, TResult>
.Create(static (TContext ctx) => /* TResult */)
.Before(static (TContext ctx) => { /* side-effect */ }) // 0..n
.After(static (TContext ctx, TResult res) => { /* side-effect */ }) // 0..n
.OnError(static (TContext ctx, string error) => { /* observe */ }) // 0..n
.Synchronized() // optional
.Build();

// Execute throws on failure
TResult result = tpl.Execute(context);

// TryExecute returns false and calls OnError hooks rather than throwing
bool ok = tpl.TryExecute(context, out TResult result, out string? error);
```

Notes
- Multiple Before/After/OnError hooks compose; registration order is call order.
- OnError hooks run only when TryExecute catches an exception.
- Synchronized() uses a per-instance lock; keep steps short to avoid contention.

---

## Testing (TinyBDD-style)

```csharp
using PatternKit.Behavioral.Template;
using TinyBDD;
using TinyBDD.Xunit;

var (tpl, calls) = (
Template<string, int>
.Create(ctx => { calls.Enqueue($"step:{ctx}"); return ctx.Length; })
.Before(ctx => calls.Enqueue($"before:{ctx}"))
.After((ctx, res) => calls.Enqueue($"after:{ctx}:{res}"))
.Build(),
new System.Collections.Concurrent.ConcurrentQueue<string>());

var r = tpl.Execute("abc"); // 3
// calls: before:abc, step:abc, after:abc:3
```

---

## Design notes

- No reflection/LINQ in the hot path; simple delegate invocation and optional lock.
- Immutable after Build() so instances can be safely shared across threads.
- Hooks are multicast; avoid heavy work inside hooks.

---

## Gotchas

- Execute throws; OnError hooks are not invoked on Execute.
- TryExecute returns default(TResult) on failure and captures ex.Message as error.
- Synchronized forces mutual exclusion; prefer idempotent, fast steps.

---

## See also

- Subclassing: [TemplateMethod<TContext, TResult>](./templatemethod.md)
- Async fluent: [AsyncTemplate<TContext, TResult>](./asynctemplate.md)
- Demos: [Template Method Demo](../../../examples/template-method-demo.md), [Template Method Async Demo](../../../examples/template-method-async-demo.md)
Loading