Decorator Pattern
Fluent wrapping and extension of components with layered behavior enhancements
Overview
The Decorator pattern allows you to attach additional responsibilities to an object dynamically. PatternKit's implementation provides a fluent, allocation-light way to wrap any component with ordered decorators that can:
- Transform input before it reaches the component (
Before) - Transform output after it returns from the component (
After) - Wrap entire execution with custom logic (
Around)
Decorators are applied as layers in registration order, making it seamless to add cross-cutting concerns like logging, caching, validation, or error handling without modifying the original component.
Mental Model
Think of decorators as layers of an onion:
- The component is at the center
- Each decorator wraps around it in the order registered
- Execution flows outward-to-inward for input transformation
- Then inward-to-outward for output transformation
┌─────────────────────────────────────┐
│ Before (outermost) │
│ ┌──────────────────────────────┐ │
│ │ Around │ │
│ │ ┌────────────────────────┐ │ │
│ │ │ After │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ Component (core) │ │ │ │
│ │ │ └──────────────────┘ │ │ │
│ │ └────────────────────────┘ │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
Quick Start
Basic Decorator
using PatternKit.Structural.Decorator;
// Wrap a simple component
var doubled = Decorator<int, int>.Create(static x => x * 2)
.Build();
var result = doubled.Execute(5); // 10
Before: Input Transformation
// Validate/transform input before component execution
var validated = Decorator<int, int>.Create(static x => 100 / x)
.Before(static x => x == 0
? throw new ArgumentException("Cannot be zero")
: x)
.Build();
var result = validated.Execute(5); // 20
// validated.Execute(0); // throws ArgumentException
After: Output Transformation
// Transform output after component execution
var enhanced = Decorator<string, int>.Create(static s => s.Length)
.Before(static s => s.Trim()) // Remove whitespace first
.After(static (input, length) => length * 2) // Double the length
.Build();
var result = enhanced.Execute(" hello "); // 10 (trimmed "hello" = 5, doubled = 10)
Around: Full Control
// Add logging around execution
var logged = Decorator<int, int>.Create(static x => x * x)
.Around((x, next) => {
Console.WriteLine($"Input: {x}");
var result = next(x);
Console.WriteLine($"Output: {result}");
return result;
})
.Build();
var squared = logged.Execute(7);
// Console output:
// Input: 7
// Output: 49
API Reference
Creating a Decorator
public static Builder Create(Component component)
Creates a new builder for decorating a component.
Parameters:
component: The base operation to decorate (signature:TOut Component(TIn input))
Returns: A fluent Builder instance
Builder Methods
Before(BeforeTransform transform)
Adds an input transformation decorator.
public Builder Before(BeforeTransform transform)
Signature: TIn BeforeTransform(TIn input)
Multiple Before decorators are applied in registration order (outermost to innermost).
Example:
.Before(static x => x + 10)
.Before(static x => x * 2) // Applied after the first Before
After(AfterTransform transform)
Adds an output transformation decorator.
public Builder After(AfterTransform transform)
Signature: TOut AfterTransform(TIn input, TOut output)
The After decorator receives both the original input and the output from inner layers.
Example:
.After(static (input, result) => result + input)
Around(AroundTransform transform)
Adds a wrapper that controls execution flow.
public Builder Around(AroundTransform transform)
Signature: TOut AroundTransform(TIn input, Component next)
The Around decorator has full control over whether and how the next layer is invoked.
Example:
.Around((x, next) => {
// Pre-processing
var result = next(x); // Invoke next layer
// Post-processing
return result;
})
Build()
Builds an immutable decorator instance.
public Decorator<TIn, TOut> Build()
Execution
public TOut Execute(in TIn input)
Executes the decorated component with the given input, applying all decorators in order.
Real-World Examples
Caching Decorator
var cache = new Dictionary<int, int>();
var cachedOperation = Decorator<int, int>.Create(x => ExpensiveComputation(x))
.Around((x, next) => {
if (cache.TryGetValue(x, out var cached))
return cached;
var result = next(x);
cache[x] = result;
return result;
})
.Build();
// First call: computes and caches
var result1 = cachedOperation.Execute(42);
// Second call: returns cached value
var result2 = cachedOperation.Execute(42);
Retry Logic
var retriable = Decorator<string, HttpResponse>.Create(url => HttpClient.Get(url))
.Around((url, next) => {
int attempts = 0;
Exception lastError = null;
while (attempts < 3) {
try {
return next(url);
} catch (Exception ex) {
lastError = ex;
attempts++;
Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, attempts)));
}
}
throw new Exception($"Failed after {attempts} attempts", lastError);
})
.Build();
Performance Monitoring
var monitored = Decorator<Query, Result>.Create(query => Database.Execute(query))
.Around((query, next) => {
var sw = Stopwatch.StartNew();
try {
var result = next(query);
Metrics.RecordSuccess(query.Name, sw.Elapsed);
return result;
} catch (Exception ex) {
Metrics.RecordFailure(query.Name, sw.Elapsed, ex);
throw;
}
})
.Build();
Authorization + Audit
var secured = Decorator<Request, Response>.Create(req => HandleRequest(req))
.Before(req => {
if (!req.User.HasPermission(req.Resource))
throw new UnauthorizedException();
return req;
})
.Around((req, next) => {
AuditLog.LogAccess(req.User, req.Resource);
var response = next(req);
AuditLog.LogSuccess(req.User, req.Resource);
return response;
})
.Build();
Circuit Breaker
var circuitBreaker = new CircuitBreakerState();
var protected = Decorator<Request, Response>.Create(req => ExternalService.Call(req))
.Around((req, next) => {
if (circuitBreaker.IsOpen)
throw new CircuitBreakerOpenException();
try {
var result = next(req);
circuitBreaker.RecordSuccess();
return result;
} catch (Exception ex) {
circuitBreaker.RecordFailure();
throw;
}
})
.Build();
Chaining Multiple Decorators
Decorators compose naturally—each decorator layer wraps the previous ones:
var fullyDecorated = Decorator<int, int>.Create(static x => x * 2)
.Before(static x => x + 1) // Validate/transform input
.Around((x, next) => { // Add caching
if (cache.TryGetValue(x, out var cached))
return cached;
var result = next(x);
cache[x] = result;
return result;
})
.Around((x, next) => { // Add logging
Log($"Calling with {x}");
var result = next(x);
Log($"Returned {result}");
return result;
})
.After(static (input, result) => result * 10) // Transform output
.Build();
// Execution order:
// 1. Before: 5 + 1 = 6
// 2. Around (logging): Log "Calling with 6"
// 3. Around (caching): Check cache, miss
// 4. Component: 6 * 2 = 12
// 5. After: 12 * 10 = 120
// 6. Around (logging): Log "Returned 120"
// 7. Return: 120
Execution Order
Multiple Before Decorators
Applied in registration order (first registered = outermost):
.Before(x => x + 10) // Applied first
.Before(x => x * 2) // Applied second to the result of first
Input 5 → 15 → 30 → component
Multiple After Decorators
Applied in registration order as layers (first registered = outermost):
.After((_, r) => r + 10) // Receives result from inner layer
.After((_, r) => r * 2) // Receives result from component
Component returns 10 → second After makes it 20 → first After makes it 30
Mixed Decorators
.Before(x => x + 1) // Layer 0 (outermost for input)
.Around((x, next) => ...) // Layer 1
.After((_, r) => ...) // Layer 2 (outermost for output)
Performance Characteristics
- Immutable after build: Thread-safe for concurrent reuse
- Minimal allocations: Decorators stored as arrays
- No reflection: Direct delegate invocations
- Struct-friendly: Can decorate value types efficiently
Best Practices
1. Use Static Lambdas Where Possible
// ✅ Good: No closure allocation
.Before(static x => x + 10)
// ❌ Avoid: Captures variable
int offset = 10;
.Before(x => x + offset)
2. Separate Concerns
Each decorator should have a single responsibility:
// ✅ Good: Separate decorators for separate concerns
var result = Decorator<T, R>.Create(component)
.Around(AddLogging)
.Around(AddCaching)
.Around(AddRetry)
.Build();
// ❌ Avoid: Multiple concerns in one decorator
.Around((x, next) => {
Log();
CheckCache();
Retry(() => next(x));
})
3. Order Matters
Consider the execution flow when ordering decorators:
// Cache should be checked BEFORE expensive retry logic
.Around(AddCaching)
.Around(AddRetry)
// Validation should happen BEFORE any processing
.Before(ValidateInput)
.Around(AddProcessing)
4. Reuse Decorators
Build once, execute many times:
// ✅ Good
var decorator = Decorator<int, int>.Create(...).Build();
for (int i = 0; i < 1000; i++)
decorator.Execute(i);
// ❌ Avoid: Rebuilding on each use
for (int i = 0; i < 1000; i++)
Decorator<int, int>.Create(...).Build().Execute(i);
Comparison with Traditional Decorator
Traditional Approach
public interface IComponent {
int Execute(int input);
}
public class ConcreteComponent : IComponent {
public int Execute(int input) => input * 2;
}
public class LoggingDecorator : IComponent {
private readonly IComponent _component;
public LoggingDecorator(IComponent component) => _component = component;
public int Execute(int input) {
Console.WriteLine($"Input: {input}");
var result = _component.Execute(input);
Console.WriteLine($"Output: {result}");
return result;
}
}
// Usage
IComponent component = new ConcreteComponent();
component = new LoggingDecorator(component);
component = new CachingDecorator(component);
var result = component.Execute(5);
PatternKit Approach
var component = Decorator<int, int>.Create(static x => x * 2)
.Around((x, next) => {
Console.WriteLine($"Input: {x}");
var result = next(x);
Console.WriteLine($"Output: {result}");
return result;
})
.Around(AddCaching)
.Build();
var result = component.Execute(5);
Benefits:
- No interface/class hierarchy needed
- Fluent, discoverable API
- Easier to compose and reorder
- Less boilerplate code
- Type-safe with full IntelliSense support
See Also
- Strategy Pattern - For conditional logic
- Chain of Responsibility - For sequential processing
- Adapter Pattern - For type conversion
- Composite Pattern - For tree structures