Skip to content

Commit 2a71bf8

Browse files
authored
Feature/26 fluent create template pattern (#56)
* feat(behavioral): add Template Method pattern - Add abstract TemplateMethod<TContext, TResult> with optional synchronization and before/after hooks - Add fluent Template<TContext, TResult> with Before/After/OnError/Synchronized and Execute/TryExecute - Add examples: subclassing demo (DataProcessor) and fluent demo (TemplateFluentDemo) - Add tests: TemplateMethodTests and TemplateFluentTests for correctness, concurrency, and error handling - Add docs for pattern and demo; update Behavioral patterns ToC and Examples index Build: PASS (multi-target) Tests: PASS (840/840) * feat(template):implemented fluent template pattern * docs: documentation updates * chore: minor code cleanup
1 parent beda683 commit 2a71bf8

40 files changed

Lines changed: 2238 additions & 934 deletions

docs/examples/index.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ Welcome! This section collects small, focused demos that show **how to compose b
4646
* **State Machine — Order Lifecycle**
4747
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.
4848

49+
* **Template Method Data Processor**
50+
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).
51+
52+
* **Template Method Async Pipeline**
53+
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).
54+
4955
## How to run
5056

5157
From the repo root:
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Template Method Async Demo
2+
3+
This demo shows non-trivial, end-to-end async workflows with PatternKit’s async Template variants, including cancellation, concurrency control, and error observation.
4+
5+
## Async subclassing demo: AsyncDataPipeline
6+
A 3-stage pipeline (fetch → transform → store) with optional serialization and cancellation.
7+
8+
```csharp
9+
public sealed class AsyncDataPipeline : AsyncTemplateMethod<int, string>
10+
{
11+
protected override bool Synchronized => false; // enable only when shared mutable state requires it
12+
13+
protected override async ValueTask OnBeforeAsync(int id, CancellationToken ct)
14+
{
15+
Console.WriteLine($"[BeforeAsync] {id}");
16+
await Task.Yield();
17+
}
18+
19+
protected override async ValueTask<string> StepAsync(int id, CancellationToken ct)
20+
{
21+
await Task.Delay(25, ct); // fetch
22+
await Task.Delay(10, ct); // transform
23+
await Task.Delay(5, ct); // store
24+
return $"VAL-{id}";
25+
}
26+
27+
protected override ValueTask OnAfterAsync(int id, string result, CancellationToken ct)
28+
{
29+
Console.WriteLine($"[AfterAsync] {id} -> {result}");
30+
return default;
31+
}
32+
}
33+
34+
var pipe = new AsyncDataPipeline();
35+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
36+
var outVal = await pipe.ExecuteAsync(42, cts.Token);
37+
```
38+
39+
## Async fluent demo: TemplateAsyncFluentDemo
40+
Same shape using the fluent builder with multiple hooks and error handling.
41+
42+
```csharp
43+
var tpl = AsyncTemplate<int, string>
44+
.Create(async (id, ct) =>
45+
{
46+
await Task.Delay(15, ct);
47+
if (id < 0) throw new InvalidOperationException("invalid id");
48+
return $"VAL-{id}";
49+
})
50+
.Before((id, ct) => { Console.WriteLine($"[BeforeAsync] {id}"); return default; })
51+
.After((id, res, ct) => { Console.WriteLine($"[AfterAsync] {id} -> {res}"); return default; })
52+
.OnError((id, err, ct) => { Console.WriteLine($"[ErrorAsync] {id}: {err}"); return default; })
53+
.Synchronized(false)
54+
.Build();
55+
56+
var (ok, res, err) = await tpl.TryExecuteAsync(42);
57+
Console.WriteLine(ok ? $"OK: {res}" : $"ERR: {err}");
58+
```
59+
60+
## Guidance
61+
- Prefer async for I/O-bound steps or where cancellation must be respected.
62+
- Use `.Synchronized()` sparingly; it introduces a critical section. Keep steps idempotent and fast.
63+
- Use `TryExecuteAsync` to keep control flow non-throwing and centralize error observation.
64+
65+
## See Also
66+
- [Template Method Pattern](../patterns/behavioral/template/templatemethod.md)
67+
- Synchronous demo: [Template Method Demo](template-method-demo.md)
68+
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Template Method Demo
2+
3+
This demo shows two ways to use PatternKit’s Template Method:
4+
5+
- Subclassing: derive from `TemplateMethod<TContext, TResult>` and override hooks.
6+
- Fluent: compose a `Template<TContext, TResult>` with `Before/After/OnError/Synchronized`.
7+
8+
Both give you a consistent workflow shape with customizable steps and optional synchronization.
9+
10+
## Subclassing demo (DataProcessor)
11+
Counts words in a string while logging before/after.
12+
13+
```csharp
14+
public class DataProcessor : TemplateMethod<string, int>
15+
{
16+
protected override void OnBefore(string context)
17+
{
18+
Console.WriteLine($"Preparing to process: {context}");
19+
}
20+
21+
protected override int Step(string context)
22+
{
23+
return context.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
24+
}
25+
26+
protected override void OnAfter(string context, int result)
27+
{
28+
Console.WriteLine($"Processed '{context}' with result: {result}");
29+
}
30+
}
31+
32+
var processor = new DataProcessor();
33+
var result = processor.Execute("The quick brown fox jumps over the lazy dog");
34+
Console.WriteLine($"Word count: {result}");
35+
```
36+
37+
## Fluent demo (TemplateFluentDemo)
38+
Same behavior using the fluent builder, plus non-throwing `TryExecute` and an `OnError` hook.
39+
40+
```csharp
41+
var tpl = Template<string, int>
42+
.Create(ctx => ctx.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length)
43+
.Before(ctx => Console.WriteLine($"[Before] Input: '{ctx}'"))
44+
.After((ctx, res) => Console.WriteLine($"[After] '{ctx}' -> words: {res}"))
45+
.OnError((ctx, err) => Console.WriteLine($"[Error] Input '{ctx}', error: {err}"))
46+
.Synchronized() // optional
47+
.Build();
48+
49+
if (tpl.TryExecute("The quick brown fox", out var count, out var error))
50+
Console.WriteLine($"Word count: {count}");
51+
else
52+
Console.WriteLine($"Failed: {error}");
53+
```
54+
55+
## When to use which
56+
- Prefer subclassing when the algorithm is a stable concept in your domain, and you want a strongly‑named type.
57+
- Prefer fluent when you want to compose quickly, add multiple hooks, or opt into `TryExecute` easily.
58+
59+
## Thread safety
60+
- Subclassing: override `Synchronized` to serialize `Execute` calls.
61+
- Fluent: call `.Synchronized()` on the builder.
62+
- Leave off for maximum concurrency when your step/hook logic is already thread-safe.
63+
64+
## See Also
65+
- [Template Method Pattern Documentation](../patterns/behavioral/template/templatemethod.md)
66+
- Refactoring Guru: Template Method — https://refactoring.guru/design-patterns/template-method

docs/examples/toc.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,9 @@
4848

4949
- name: Async Connection State Machine
5050
href: async-state-machine.md
51+
52+
- name: Template Method (Subclassing)
53+
href: template-method-demo.md
54+
55+
- name: Template Method (Async)
56+
href: template-method-async-demo.md
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# AsyncTemplate<TContext, TResult>
2+
3+
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.
4+
5+
---
6+
7+
## What it is
8+
9+
- Async skeleton with three phases: Before (0..n), Step (1), After (0..n)
10+
- Non-throwing path via `TryExecuteAsync(context)` returning `(ok, result, error)`
11+
- Optional per-instance synchronization via `SemaphoreSlim`
12+
- Immutable and thread-safe after `Build()`
13+
14+
---
15+
16+
## TL;DR
17+
18+
```csharp
19+
using PatternKit.Behavioral.Template;
20+
21+
var tpl = AsyncTemplate<int, string>
22+
.Create(async (id, ct) =>
23+
{
24+
await Task.Delay(15, ct);
25+
if (id < 0) throw new InvalidOperationException("invalid id");
26+
return $"VAL-{id}";
27+
})
28+
.Before((id, ct) => { Console.WriteLine($"[BeforeAsync] {id}"); return default; })
29+
.After((id, res, ct) => { Console.WriteLine($"[AfterAsync] {id} -> {res}"); return default; })
30+
.OnError((id, err, ct) => { Console.WriteLine($"[ErrorAsync] {id}: {err}"); return default; })
31+
.Synchronized() // optional
32+
.Build();
33+
34+
var (ok, result, error) = await tpl.TryExecuteAsync(42);
35+
Console.WriteLine(ok ? $"OK: {result}" : $"ERR: {error}");
36+
```
37+
38+
---
39+
40+
## API shape
41+
42+
```csharp
43+
var tpl = AsyncTemplate<TContext, TResult>
44+
.Create(static (TContext ctx, CancellationToken ct) => /* ValueTask<TResult> */)
45+
.Before(static (TContext ctx, CancellationToken ct) => /* ValueTask */) // 0..n (async)
46+
.Before(static (TContext ctx) => { /* side-effect */ }) // 0..n (sync overload)
47+
.After(static (TContext ctx, TResult res, CancellationToken ct) => /* ValueTask */) // 0..n (async)
48+
.After(static (TContext ctx, TResult res) => { /* side-effect */ }) // 0..n (sync overload)
49+
.OnError(static (TContext ctx, string error, CancellationToken ct) => /* ValueTask */) // 0..n (async)
50+
.OnError(static (TContext ctx, string error) => { /* observe */ }) // 0..n (sync overload)
51+
.Synchronized() // optional
52+
.Build();
53+
54+
// Throws on failure
55+
TResult result = await tpl.ExecuteAsync(context, ct);
56+
57+
// Non-throwing; returns tuple (ok, result?, error?)
58+
(bool ok, TResult? result, string? error) = await tpl.TryExecuteAsync(context, ct);
59+
```
60+
61+
Notes
62+
- Multiple hooks compose; registration order is invocation order.
63+
- OnError hooks run only when TryExecuteAsync catches an exception.
64+
- Synchronized() uses an async mutex; keep the critical section small.
65+
66+
---
67+
68+
## Testing (TinyBDD-style)
69+
70+
```csharp
71+
using PatternKit.Behavioral.Template;
72+
using TinyBDD;
73+
using TinyBDD.Xunit;
74+
75+
var tpl = AsyncTemplate<string, int>
76+
.Create(async (ctx, ct) => { await Task.Yield(); return ctx.Length; })
77+
.Before((ctx, ct) => { Console.WriteLine($"before:{ctx}"); return default; })
78+
.After((ctx, res, ct) => { Console.WriteLine($"after:{ctx}:{res}"); return default; })
79+
.Build();
80+
81+
var r = await tpl.ExecuteAsync("abc"); // 3
82+
```
83+
84+
---
85+
86+
## Design notes
87+
88+
- No reflection/LINQ in the hot path; simple async delegate calls and an optional async lock.
89+
- Immutable after Build() so instances can be safely shared across threads.
90+
- Sync and async hooks both supported; they are adapted internally to async.
91+
92+
---
93+
94+
## Gotchas
95+
96+
- ExecuteAsync throws; OnError hooks are not invoked by ExecuteAsync.
97+
- TryExecuteAsync captures ex.Message as error; result is default when failing.
98+
- Synchronized serializes executions; prefer idempotent, short steps.
99+
100+
---
101+
102+
## See also
103+
104+
- Subclassing: [TemplateMethod<TContext, TResult>](./templatemethod.md)
105+
- Synchronous fluent: [Template<TContext, TResult>](./template.md)
106+
- Demos: [Template Method Demo](../../../examples/template-method-demo.md), [Template Method Async Demo](../../../examples/template-method-async-demo.md)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Template<TContext, TResult>
2+
3+
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.
4+
5+
---
6+
7+
## What it is
8+
9+
- Skeleton with three phases: Before (0..n), Step (1), After (0..n)
10+
- Non-throwing path via TryExecute(context, out result, out error)
11+
- Optional per-instance synchronization (mutual exclusion)
12+
- Immutable and thread-safe after Build()
13+
14+
---
15+
16+
## TL;DR
17+
18+
```csharp
19+
using PatternKit.Behavioral.Template;
20+
21+
var tpl = Template<string, int>
22+
.Create(ctx => ctx.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length)
23+
.Before(ctx => Console.WriteLine($"[Before] '{ctx}'"))
24+
.After((ctx, res) => Console.WriteLine($"[After] '{ctx}' -> {res}"))
25+
.OnError((ctx, err) => Console.WriteLine($"[Error] '{ctx}': {err}"))
26+
.Synchronized() // optional
27+
.Build();
28+
29+
var ok = tpl.TryExecute("The quick brown fox", out var words, out var error);
30+
Console.WriteLine(ok ? $"Words={words}" : $"Failed: {error}");
31+
```
32+
33+
---
34+
35+
## API shape
36+
37+
```csharp
38+
var tpl = Template<TContext, TResult>
39+
.Create(static (TContext ctx) => /* TResult */)
40+
.Before(static (TContext ctx) => { /* side-effect */ }) // 0..n
41+
.After(static (TContext ctx, TResult res) => { /* side-effect */ }) // 0..n
42+
.OnError(static (TContext ctx, string error) => { /* observe */ }) // 0..n
43+
.Synchronized() // optional
44+
.Build();
45+
46+
// Execute throws on failure
47+
TResult result = tpl.Execute(context);
48+
49+
// TryExecute returns false and calls OnError hooks rather than throwing
50+
bool ok = tpl.TryExecute(context, out TResult result, out string? error);
51+
```
52+
53+
Notes
54+
- Multiple Before/After/OnError hooks compose; registration order is call order.
55+
- OnError hooks run only when TryExecute catches an exception.
56+
- Synchronized() uses a per-instance lock; keep steps short to avoid contention.
57+
58+
---
59+
60+
## Testing (TinyBDD-style)
61+
62+
```csharp
63+
using PatternKit.Behavioral.Template;
64+
using TinyBDD;
65+
using TinyBDD.Xunit;
66+
67+
var (tpl, calls) = (
68+
Template<string, int>
69+
.Create(ctx => { calls.Enqueue($"step:{ctx}"); return ctx.Length; })
70+
.Before(ctx => calls.Enqueue($"before:{ctx}"))
71+
.After((ctx, res) => calls.Enqueue($"after:{ctx}:{res}"))
72+
.Build(),
73+
new System.Collections.Concurrent.ConcurrentQueue<string>());
74+
75+
var r = tpl.Execute("abc"); // 3
76+
// calls: before:abc, step:abc, after:abc:3
77+
```
78+
79+
---
80+
81+
## Design notes
82+
83+
- No reflection/LINQ in the hot path; simple delegate invocation and optional lock.
84+
- Immutable after Build() so instances can be safely shared across threads.
85+
- Hooks are multicast; avoid heavy work inside hooks.
86+
87+
---
88+
89+
## Gotchas
90+
91+
- Execute throws; OnError hooks are not invoked on Execute.
92+
- TryExecute returns default(TResult) on failure and captures ex.Message as error.
93+
- Synchronized forces mutual exclusion; prefer idempotent, fast steps.
94+
95+
---
96+
97+
## See also
98+
99+
- Subclassing: [TemplateMethod<TContext, TResult>](./templatemethod.md)
100+
- Async fluent: [AsyncTemplate<TContext, TResult>](./asynctemplate.md)
101+
- Demos: [Template Method Demo](../../../examples/template-method-demo.md), [Template Method Async Demo](../../../examples/template-method-async-demo.md)

0 commit comments

Comments
 (0)