Table of Contents

Factory Pattern Guide

Comprehensive guide to using the Factory pattern in PatternKit.

Overview

Factory provides an immutable registry mapping keys to creator delegates. It's ideal for type registration, plugin systems, and strategy selection by key.

flowchart LR
    K[Key] --> F{Factory}
    F -->|json| C1[JSON Creator]
    F -->|xml| C2[XML Creator]
    F -->|unknown| D[Default Creator]

Getting Started

Installation

using PatternKit.Creational.Factory;

Basic Usage

// Simple factory (parameterless creators)
var shapes = Factory<string, IShape>
    .Create()
    .Map("circle", () => new Circle())
    .Map("square", () => new Square())
    .Default(() => new UnknownShape())
    .Build();

var circle = shapes.Create("circle");

Factory with Input

// Factory with input parameter
var math = Factory<string, int, int>
    .Create()
    .Map("double", (in int x) => x * 2)
    .Map("square", (in int x) => x * x)
    .Default((in int x) => x)
    .Build();

var result = math.Create("double", 5); // 10

Core Concepts

Key Mapping

Map keys to creator delegates:

var factory = Factory<string, IService>
    .Create()
    .Map("serviceA", () => new ServiceA())
    .Map("serviceB", () => new ServiceB())
    .Build();

Default Handling

Provide a fallback for unknown keys:

// With default - returns fallback for unknown keys
var withDefault = Factory<string, string>
    .Create()
    .Map("a", () => "A")
    .Default(() => "unknown")
    .Build();

var result = withDefault.Create("xyz"); // "unknown"

// Without default - throws for unknown keys
var withoutDefault = Factory<string, string>
    .Create()
    .Map("a", () => "A")
    .Build();

withoutDefault.Create("xyz"); // Throws InvalidOperationException

Key Comparers

Control key matching:

// Case-insensitive keys
var factory = Factory<string, string>
    .Create(StringComparer.OrdinalIgnoreCase)
    .Map("json", () => "application/json")
    .Map("xml", () => "application/xml")
    .Build();

factory.Create("JSON"); // "application/json"
factory.Create("Json"); // "application/json"

Common Patterns

Content Type Registry

public class ContentTypeFactory
{
    private readonly Factory<string, string> _factory;

    public ContentTypeFactory()
    {
        _factory = Factory<string, string>
            .Create(StringComparer.OrdinalIgnoreCase)
            .Map("json", () => "application/json")
            .Map("xml", () => "application/xml")
            .Map("html", () => "text/html; charset=utf-8")
            .Map("css", () => "text/css")
            .Map("js", () => "application/javascript")
            .Map("png", () => "image/png")
            .Map("jpg", () => "image/jpeg")
            .Map("jpeg", () => "image/jpeg")
            .Map("gif", () => "image/gif")
            .Map("svg", () => "image/svg+xml")
            .Map("pdf", () => "application/pdf")
            .Default(() => "application/octet-stream")
            .Build();
    }

    public string GetContentType(string extension) =>
        _factory.Create(extension.TrimStart('.'));
}

Serializer Registry

public interface ISerializer
{
    string Serialize<T>(T obj);
    T Deserialize<T>(string data);
}

public class SerializerFactory
{
    private readonly Factory<string, ISerializer> _factory;

    public SerializerFactory()
    {
        _factory = Factory<string, ISerializer>
            .Create(StringComparer.OrdinalIgnoreCase)
            .Map("json", () => new JsonSerializer())
            .Map("xml", () => new XmlSerializer())
            .Map("yaml", () => new YamlSerializer())
            .Build();
    }

    public ISerializer GetSerializer(string format) =>
        _factory.Create(format);

    public bool TryGetSerializer(string format, out ISerializer serializer) =>
        _factory.TryCreate(format, out serializer);
}

Parser Factory with Context

public record ParseContext(string Source, CultureInfo Culture);

public class ParserFactory
{
    private readonly Factory<Type, ParseContext, object> _factory;

    public ParserFactory()
    {
        _factory = Factory<Type, ParseContext, object>
            .Create()
            .Map(typeof(int), (in ParseContext ctx) =>
                int.Parse(ctx.Source, ctx.Culture))
            .Map(typeof(decimal), (in ParseContext ctx) =>
                decimal.Parse(ctx.Source, ctx.Culture))
            .Map(typeof(DateTime), (in ParseContext ctx) =>
                DateTime.Parse(ctx.Source, ctx.Culture))
            .Map(typeof(bool), (in ParseContext ctx) =>
                bool.Parse(ctx.Source))
            .Default((in ParseContext ctx) => ctx.Source)
            .Build();
    }

    public T Parse<T>(string source, CultureInfo culture = null) =>
        (T)_factory.Create(typeof(T), new ParseContext(source, culture ?? CultureInfo.InvariantCulture));
}

Plugin System

public interface IPlugin
{
    string Name { get; }
    void Execute();
}

public class PluginRegistry
{
    private Factory<string, IPlugin> _factory;

    public PluginRegistry()
    {
        var builder = Factory<string, IPlugin>.Create(StringComparer.OrdinalIgnoreCase);

        // Discover and register plugins
        foreach (var type in DiscoverPluginTypes())
        {
            var plugin = (IPlugin)Activator.CreateInstance(type);
            builder.Map(plugin.Name, () => (IPlugin)Activator.CreateInstance(type));
        }

        _factory = builder.Build();
    }

    public IPlugin GetPlugin(string name) => _factory.Create(name);

    public bool TryGetPlugin(string name, out IPlugin plugin) =>
        _factory.TryCreate(name, out plugin);
}

Best Practices

Use Static Lambdas

Avoid closure allocations:

// Good - static lambdas
.Map("key", static () => new Service())

// Avoid - may capture variables
var dep = new Dependency();
.Map("key", () => new Service(dep))  // Captures dep

Prefer TryCreate for Probing

// When you expect missing keys
if (factory.TryCreate(key, out var result))
{
    // Use result
}
else
{
    // Handle missing gracefully
}

// When missing is exceptional
var result = factory.Create(key); // Throws if missing

Immutability

Factories are immutable after Build():

var builder = Factory<string, int>.Create();
builder.Map("a", () => 1);
var factory1 = builder.Build();

builder.Map("b", () => 2);  // Doesn't affect factory1
var factory2 = builder.Build();

// factory1 only has "a"
// factory2 has "a" and "b"

Troubleshooting

InvalidOperationException: No mapping

No mapping exists and no default configured:

// Add a default
.Default(() => fallbackValue)

// Or use TryCreate
if (!factory.TryCreate(key, out var result))
    // Handle missing

Wrong value returned

Check key comparer:

// Case-sensitive by default
.Create()  // "JSON" != "json"

// Use case-insensitive for strings
.Create(StringComparer.OrdinalIgnoreCase)

Last mapping wins

Later mappings replace earlier ones:

.Map("key", () => 1)
.Map("key", () => 2)  // This wins

See Also