Prototype Pattern Guide
Comprehensive guide to using the Prototype pattern in PatternKit.
Overview
Prototype creates new objects by cloning a prototypical instance rather than calling constructors. This implementation adds fluent mutations, registry support, and immutable configuration.
flowchart LR
subgraph Prototype
S[Source] --> C[Cloner]
C --> N[Clone]
N --> DM[Default Mutations]
DM --> PM[Per-Call Mutations]
end
PM --> R[Result]
Getting Started
Installation
using PatternKit.Creational.Prototype;
Basic Usage
// Define a cloneable type
public class Widget
{
public string Name { get; set; }
public int Size { get; set; }
}
// Create prototype with cloner
var proto = Prototype<Widget>
.Create(new Widget { Name = "base", Size = 1 }, Clone)
.With(w => w.Size++) // default mutation
.Build();
// Clone instances
var a = proto.Create(); // Size=2
var b = proto.Create(w => w.Size += 10); // Size=12
static Widget Clone(in Widget w) => new() { Name = w.Name, Size = w.Size };
Core Concepts
The Cloner Delegate
You provide a Cloner delegate that copies the source instance:
public delegate T Cloner(in T source);
The in parameter avoids struct copies. Common implementations:
// For classes - shallow copy
static Widget Clone(in Widget w) => new() { Name = w.Name, Size = w.Size };
// For records - use with expression
static Config Clone(in Config c) => c with { };
// For deep copy with nested objects
static Order Clone(in Order o) => new()
{
Id = o.Id,
Items = o.Items.Select(i => new OrderItem { Sku = i.Sku, Qty = i.Qty }).ToList()
};
Default Mutations
Mutations applied to every clone:
var proto = Prototype<Widget>
.Create(source, Clone)
.With(w => w.CreatedAt = DateTime.UtcNow) // always set
.With(w => w.Id = Guid.NewGuid()) // always unique
.Build();
Mutations compose in order:
.With(w => w.Size = 10) // First
.With(w => w.Size *= 2) // Second: Size becomes 20
Per-Call Mutations
Additional mutations for specific clones:
// Use default mutations only
var standard = proto.Create();
// Add extra mutation for this clone
var special = proto.Create(w => w.Size = 100);
Order: default mutations run first, then per-call.
Prototype Registry
For multiple prototype families by key:
enum ShapeKind { Circle, Square, Triangle }
var shapes = Prototype<ShapeKind, Shape>
.Create()
.Map(ShapeKind.Circle, new Circle { Radius = 1 }, CloneCircle)
.Map(ShapeKind.Square, new Square { Side = 1 }, CloneSquare)
.Mutate(ShapeKind.Circle, s => s.Color = "red") // default for circles
.Default(new Shape { Name = "unknown" }, CloneShape)
.Build();
Registry Methods
// Direct creation (throws if missing without default)
var circle = shapes.Create(ShapeKind.Circle);
// With per-call mutation
var bigSquare = shapes.Create(ShapeKind.Square, s => s.Side = 10);
// Safe creation
if (shapes.TryCreate(ShapeKind.Triangle, out var triangle))
{
// Use triangle
}
Key Comparers
// Case-insensitive string keys
var protos = Prototype<string, Config>
.Create(StringComparer.OrdinalIgnoreCase)
.Map("DEV", devConfig, Clone)
.Map("PROD", prodConfig, Clone)
.Build();
var cfg = protos.Create("dev"); // Matches "DEV"
Common Patterns
Test Data Builder
public class TestDataFactory
{
private readonly Prototype<Customer> _customer;
private readonly Prototype<Order> _order;
public TestDataFactory()
{
_customer = Prototype<Customer>
.Create(new Customer
{
Name = "Test User",
Email = "test@example.com",
Status = CustomerStatus.Active
}, Clone)
.With(c => c.Id = Guid.NewGuid())
.Build();
_order = Prototype<Order>
.Create(new Order
{
Status = OrderStatus.Pending,
Items = new List<OrderItem>()
}, CloneOrder)
.With(o => o.Id = Guid.NewGuid())
.With(o => o.CreatedAt = DateTime.UtcNow)
.Build();
}
public Customer CreateCustomer(Action<Customer>? customize = null)
=> _customer.Create(customize);
public Order CreateOrder(Action<Order>? customize = null)
=> _order.Create(customize);
// Usage in tests
// var customer = factory.CreateCustomer(c => c.Name = "VIP Customer");
// var order = factory.CreateOrder(o => o.Items.Add(testItem));
}
Configuration Templates
var configs = Prototype<string, AppConfig>
.Create(StringComparer.OrdinalIgnoreCase)
.Map("development", new AppConfig
{
LogLevel = LogLevel.Debug,
ConnectionString = "Server=localhost;...",
EnableCaching = false
}, Clone)
.Map("production", new AppConfig
{
LogLevel = LogLevel.Warning,
ConnectionString = "Server=proddb;...",
EnableCaching = true
}, Clone)
.Mutate("development", c => c.FeatureFlags["debug"] = true)
.Default(new AppConfig { LogLevel = LogLevel.Information }, Clone)
.Build();
var devConfig = configs.Create("development");
var prodConfig = configs.Create("production", c => c.Region = "us-east-1");
Game Entity Spawning
enum EnemyType { Goblin, Orc, Dragon }
var enemies = Prototype<EnemyType, Enemy>
.Create()
.Map(EnemyType.Goblin, new Enemy
{
Name = "Goblin",
Health = 30,
Damage = 5,
Speed = 1.5f
}, Clone)
.Map(EnemyType.Orc, new Enemy
{
Name = "Orc",
Health = 80,
Damage = 15,
Speed = 1.0f
}, Clone)
.Map(EnemyType.Dragon, new Enemy
{
Name = "Dragon",
Health = 500,
Damage = 50,
Speed = 2.0f,
Abilities = new[] { "fire-breath", "fly" }
}, CloneEnemy)
.Build();
// Spawn with position
var goblin = enemies.Create(EnemyType.Goblin, e =>
{
e.Id = Guid.NewGuid();
e.Position = spawnPoint;
});
Document Templates
var templates = Prototype<string, Document>
.Create(StringComparer.OrdinalIgnoreCase)
.Map("invoice", new Document
{
Type = "Invoice",
Template = LoadTemplate("invoice.html"),
Styles = InvoiceStyles,
Footer = "Thank you for your business"
}, Clone)
.Map("report", new Document
{
Type = "Report",
Template = LoadTemplate("report.html"),
Styles = ReportStyles,
Header = "Confidential"
}, Clone)
.Build();
var invoice = templates.Create("invoice", d =>
{
d.Title = "Invoice #12345";
d.Data = orderData;
});
Best Practices
Use Static Lambdas for Cloners
// Good - static, no closure
var proto = Prototype<Widget>
.Create(source, static (in Widget w) => new Widget
{
Name = w.Name,
Size = w.Size
})
.Build();
// Avoid - may capture variables
var multiplier = 2;
.With(w => w.Size *= multiplier) // Captures multiplier
Prefer Records for Simple Cloning
public record Config(string Name, int Value);
var proto = Prototype<Config>
.Create(new Config("base", 0), static (in Config c) => c with { })
.With(c => /* record properties are init-only, use new record */)
.Build();
Validate Before Building
var builder = Prototype<EnemyType, Enemy>.Create();
// Map must be called before Mutate for each key
builder.Map(EnemyType.Goblin, goblinSource, Clone);
builder.Mutate(EnemyType.Goblin, e => e.Buffed = true);
// This would throw at Build() time:
// builder.Mutate(EnemyType.Orc, e => e.Buffed = true); // No Map for Orc!
var proto = builder.Build();
Thread Safety
| Component | Thread-Safe |
|---|---|
Builder |
No - single-threaded configuration |
Prototype<T> |
Yes - immutable after build |
Prototype<TKey, T> |
Yes - immutable after build |
Create |
Yes - cloner/mutations may not be |
Note: While the prototype registry is thread-safe, your cloner and mutation delegates must also be thread-safe if called from multiple threads.
Troubleshooting
InvalidOperationException: No prototype family
No mapping exists for the key and no default configured:
// Add a default
.Default(fallbackSource, Clone)
// Or use TryCreate
if (!proto.TryCreate(key, out var value))
// Handle missing
InvalidOperationException: Map must be called before Mutate
Mutate was called for a key without first calling Map:
// Wrong order
.Mutate(key, m => ...) // No source/cloner yet!
.Map(key, source, Clone)
// Correct order
.Map(key, source, Clone)
.Mutate(key, m => ...)
Clone modifications affect source
Your cloner is returning the same reference, not a new instance:
// Wrong - returns same instance
static Widget WrongClone(in Widget w) => w;
// Correct - creates new instance
static Widget Clone(in Widget w) => new() { Name = w.Name, Size = w.Size };