Table of Contents

Template Method Pattern

Category: Behavioral


What it is

Template Method defines the skeleton of an algorithm in a base class, allowing specific steps to be customized without changing the overall structure. PatternKit offers two complementary shapes:

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

Common traits:

  • Generic and type-safe (any context/result types)
  • Allocation-light, production-shaped APIs
  • Optional synchronization for thread safety
  • Clear separation of “when/where” (hooks) and “what” (the main step)

TL;DR (subclassing)

using PatternKit.Behavioral.Template;

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

    protected override int Step(string context)
        => context.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;

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

    // Optional: serialize concurrent Execute calls
    protected override bool Synchronized => true;
}

var processor = new DataProcessor();
var count = processor.Execute("The quick brown fox");

API shape (subclassing)

  • TemplateMethod<TContext, TResult> (abstract)

    • TResult Execute(TContext context) — calls OnBefore, Step, OnAfter in order
    • protected virtual void OnBefore(TContext context) — optional pre-step hook
    • protected abstract TResult Step(TContext context) — required main step
    • protected virtual void OnAfter(TContext context, TResult result) — optional post-step hook
    • protected virtual bool Synchronized — set to true to serialize executions
  • AsyncTemplateMethod<TContext, TResult> (abstract)

    • Task<TResult> ExecuteAsync(TContext context, CancellationToken ct) — calls OnBeforeAsync, StepAsync, OnAfterAsync
    • protected virtual ValueTask OnBeforeAsync(TContext context, CancellationToken ct) — optional pre-step hook
    • protected abstract ValueTask<TResult> StepAsync(TContext context, CancellationToken ct) — required main step
    • protected virtual ValueTask OnAfterAsync(TContext context, TResult result, CancellationToken ct) — optional post-step hook
    • protected virtual bool Synchronized — set to true to serialize ExecuteAsync calls (uses SemaphoreSlim)

Prefer the fluent siblings when you want multicast hooks, non-throwing Try execution, or quick composition: see Template<TContext, TResult> and AsyncTemplate<TContext, TResult>.


Fluent Example

using PatternKit.Behavioral.Template;

var template = 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();

if (template.TryExecute("The quick brown fox", out var result, out var error))
    Console.WriteLine($"Words: {result}");
else
    Console.WriteLine($"Failed: {error}");

Async variants

PatternKit also provides first-class async variants with cancellation and optional synchronization:

Async subclassing example

using PatternKit.Behavioral.Template;

public sealed class AsyncDataPipeline : AsyncTemplateMethod<int, string>
{
    protected override bool Synchronized => false; // enable for strict serialization

    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; // completed
    }
}

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

Async fluent example

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);

Guidance

  • Prefer async variants for I/O-bound steps or when cancellation needs to flow end-to-end.
  • Use .Synchronized() or override Synchronized only when shared mutable state demands serialization.
  • Choose TryExecuteAsync when you need non-throwing control flow and centralized error observation.

When to Use

  • You need a consistent workflow with customizable steps.
  • You want to prevent structural drift while enabling tailored behaviors.
  • You need optional error handling and synchronization without external plumbing.

Thread Safety

  • Subclassing: override Synchronized to serialize Execute calls via a per-instance lock.
  • Fluent: call .Synchronized() on the builder to enable a per-instance lock.
  • For stateless or externally synchronized code, leave synchronization off for maximal concurrency.

Error Handling

  • Subclassing: let exceptions bubble; catch externally if needed.
  • Fluent: use TryExecute to avoid throwing, and .OnError(...) to observe errors.

  • Strategy: swap entire algorithms rather than customizing steps inline.
  • Chain of Responsibility: linear rule packs with stop/continue semantics.
  • State: behavior that changes with state; Template Method keeps structure fixed.

See Also