Creational Patterns
Design patterns from “Design Patterns: Elements of Reusable Object-Oriented Software” by the Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides), published 1994
Creational patterns focus on object creation mechanisms, providing flexibility in how objects are instantiated.
Historical note: While the Gang of Four catalog formalized these patterns, many existed in practice before 1994. The patterns book itself was inspired by Christopher Alexander’s “A Pattern Language” (1977) for architecture.
Builder Pattern
Purpose: Construct complex objects step by step, separating construction from representation.
Modern Variations:
Fluent Builder
public class HtmlElement
{
public string TagName { get; set; }
public string Content { get; set; }
public Dictionary<string, string> Attributes { get; set; } = new();
public List<HtmlElement> Children { get; set; } = new();
}
public class HtmlBuilder
{
private readonly HtmlElement element = new();
public HtmlBuilder SetTag(string tagName)
{
element.TagName = tagName;
return this;
}
public HtmlBuilder AddClass(string className)
{
element.Attributes["class"] = element.Attributes.ContainsKey("class")
? $"{element.Attributes["class"]} {className}"
: className;
return this;
}
public HtmlBuilder AddContent(string content)
{
element.Content = content;
return this;
}
public HtmlElement Build() => element;
}
// Usage:
var html = new HtmlBuilder()
.SetTag("div")
.AddClass("container")
.AddContent("Hello World")
.Build();
Stepwise Builder
public interface ICarType
{
IWheelSize OfType(string carType);
}
public interface IWheelSize
{
IBuildable WithWheels(int wheelSize);
}
public interface IBuildable
{
Car Build();
}
public class CarBuilder : ICarType, IWheelSize, IBuildable
{
private readonly Car car = new();
public static ICarType Create() => new CarBuilder();
public IWheelSize OfType(string carType)
{
car.Type = carType;
return this;
}
public IBuildable WithWheels(int wheelSize)
{
car.WheelSize = wheelSize;
return this;
}
public Car Build() => car;
}
// Usage: CarBuilder.Create().OfType("SUV").WithWheels(18).Build()
Functional Builder
public class PersonBuilder
{
private readonly List<Action<Person>> actions = new();
public PersonBuilder Called(string name)
{
actions.Add(p => p.Name = name);
return this;
}
public PersonBuilder WorksAs(string position)
{
actions.Add(p => p.Position = position);
return this;
}
public PersonBuilder Earning(decimal salary)
{
actions.Add(p => p.Salary = salary);
return this;
}
public Person Build()
{
var person = new Person();
actions.ForEach(a => a(person));
return person;
}
}
// Usage:
var person = new PersonBuilder()
.Called("John")
.WorksAs("Developer")
.Earning(75000)
.Build();
Factory Patterns
Factory Method
public class Person
{
public string Name { get; set; }
public string Role { get; set; }
public decimal Salary { get; set; }
private Person() { } // Private constructor
public static Person NewCustomer(string name)
{
return new Person { Name = name, Role = "Customer", Salary = 0 };
}
public static Person NewEmployee(string name, decimal salary)
{
return new Person { Name = name, Role = "Employee", Salary = salary };
}
}
// Usage:
var customer = Person.NewCustomer("Alice");
var employee = Person.NewEmployee("Bob", 50000);
Abstract Factory
// Abstract products
public interface IButton
{
void Render();
}
public interface ICheckbox
{
void Render();
}
// Concrete products for Windows
public class WindowsButton : IButton
{
public void Render() => Console.WriteLine("Rendering Windows button");
}
public class WindowsCheckbox : ICheckbox
{
public void Render() => Console.WriteLine("Rendering Windows checkbox");
}
// Concrete products for Mac
public class MacButton : IButton
{
public void Render() => Console.WriteLine("Rendering Mac button");
}
public class MacCheckbox : ICheckbox
{
public void Render() => Console.WriteLine("Rendering Mac checkbox");
}
// Abstract factory
public interface IUIFactory
{
IButton CreateButton();
ICheckbox CreateCheckbox();
}
// Concrete factories
public class WindowsUIFactory : IUIFactory
{
public IButton CreateButton() => new WindowsButton();
public ICheckbox CreateCheckbox() => new WindowsCheckbox();
}
public class MacUIFactory : IUIFactory
{
public IButton CreateButton() => new MacButton();
public ICheckbox CreateCheckbox() => new MacCheckbox();
}
// Usage:
IUIFactory factory = Environment.OSVersion.Platform == PlatformID.Win32NT
? new WindowsUIFactory()
: new MacUIFactory();
var button = factory.CreateButton();
var checkbox = factory.CreateCheckbox();
Async Factory
public class DatabaseConnection
{
private string connectionString;
private DatabaseConnection(string connectionString)
{
this.connectionString = connectionString;
}
public static async Task<DatabaseConnection> CreateAsync(string server, string database)
{
// Simulate async connection validation
await Task.Delay(100);
var connectionString = $"Server={server};Database={database}";
var connection = new DatabaseConnection(connectionString);
// Additional async initialization
await connection.InitializeAsync();
return connection;
}
private async Task InitializeAsync()
{
// Async initialization logic
await Task.Delay(50);
}
}
// Usage:
var connection = await DatabaseConnection.CreateAsync("localhost", "MyDB");
Singleton Pattern
One of the original Gang of Four patterns (1994), but now controversial in modern software development
⚠️ Modern Warning: Traditional singleton implementations are difficult to test and violate dependency inversion. Many consider Singleton an anti-pattern in modern development due to:
- Global state (hidden dependencies)
- Testing difficulties (can’t mock easily)
- Violates Single Responsibility (manages own lifecycle + business logic)
- Thread-safety complexity in some languages
Modern consensus: Use dependency injection with singleton lifetime instead of static Singleton pattern.
Traditional Singleton (Avoid)
public sealed class Database
{
private static readonly Lazy<Database> instance = new(() => new Database());
public static Database Instance => instance.Value;
private Database() { }
public void Query(string sql) { /* implementation */ }
}
// Problem: Hard to test, violates DI
Recommended Approaches:
Dependency Injection (Preferred)
public interface IDatabase
{
void Query(string sql);
}
public class Database : IDatabase
{
public void Query(string sql) { /* implementation */ }
}
// In Startup.cs or Program.cs
services.AddSingleton<IDatabase, Database>();
// In consuming class
public class UserService
{
private readonly IDatabase database;
public UserService(IDatabase database)
{
this.database = database;
}
}
Thread-Safe Lazy Initialization
public class ConfigurationManager
{
private static readonly Lazy<ConfigurationManager> instance =
new(() => new ConfigurationManager());
public static ConfigurationManager Instance => instance.Value;
private readonly Dictionary<string, string> settings = new();
private ConfigurationManager()
{
LoadConfiguration();
}
private void LoadConfiguration()
{
// Load from file, database, etc.
}
public string GetSetting(string key) => settings.TryGetValue(key, out var value) ? value : "";
}
Alternative Patterns:
Per-Thread Singleton
public class ThreadLocalSingleton
{
private static readonly ThreadLocal<ThreadLocalSingleton> instance =
new(() => new ThreadLocalSingleton());
public static ThreadLocalSingleton Instance => instance.Value;
private ThreadLocalSingleton() { }
}
Ambient Context
public class DatabaseContext : IDisposable
{
private static readonly Stack<DatabaseContext> contexts = new();
public static DatabaseContext Current => contexts.Count > 0 ? contexts.Peek() : null;
public DatabaseContext()
{
contexts.Push(this);
}
public void Dispose()
{
if (contexts.Count > 0 && contexts.Peek() == this)
contexts.Pop();
}
}
// Usage:
using (new DatabaseContext())
{
var current = DatabaseContext.Current; // Gets the current context
// Nested contexts work automatically
using (new DatabaseContext())
{
var nested = DatabaseContext.Current; // Gets the nested context
}
// Back to original context
}
Prototype Pattern
Purpose: Create new objects by copying existing instances rather than creating from scratch.
public interface IPrototype<T>
{
T Clone();
}
public class Person : IPrototype<Person>
{
public string Name { get; set; }
public int Age { get; set; }
public Address Address { get; set; }
// Deep clone implementation
public Person Clone()
{
return new Person
{
Name = this.Name,
Age = this.Age,
Address = this.Address?.Clone() // Assuming Address also implements IPrototype
};
}
}
public class Address : IPrototype<Address>
{
public string Street { get; set; }
public string City { get; set; }
public Address Clone()
{
return new Address
{
Street = this.Street,
City = this.City
};
}
}
// Modern JSON-based approach (simpler but requires serializable types)
public static class PrototypeHelper
{
public static T DeepClone<T>(this T source) where T : class
{
var serialized = JsonSerializer.Serialize(source);
return JsonSerializer.Deserialize<T>(serialized);
}
}
// Usage:
var original = new Person { Name = "John", Age = 30, Address = new Address { Street = "123 Main St", City = "NYC" } };
var clone1 = original.Clone(); // Using interface
var clone2 = original.DeepClone(); // Using JSON serialization
Quick Reference
Creational Pattern Comparison
Pattern | Intent | Problem Solved | When to Use | When to Avoid |
---|---|---|---|---|
Factory Method | Define object creation interface | Multiple ways to create objects | Subclasses determine which class to instantiate | Simple constructor is sufficient |
Abstract Factory | Create families of related objects | Need consistent object families | Cross-platform UIs, themed components | Only one product family |
Builder | Construct complex objects step-by-step | Objects with many optional parameters | Immutable objects, fluent APIs, complex construction | Simple objects with few properties |
Prototype | Clone existing objects | Expensive object creation | Object templates, reducing initialization cost | Objects are cheap to create |
Singleton | Single instance globally | Shared resource access | Config, logging - prefer DI instead | Almost always - use DI |
Pattern Selection Guide
Choose Factory Method when:
- You have multiple construction approaches
- Subclasses should decide what to instantiate
- Example:
Person.NewCustomer()
,Person.NewEmployee()
Choose Abstract Factory when:
- You need families of related objects
- Products must be used together
- Example: UI controls for Windows vs Mac
Choose Builder when:
- Object has many optional parameters (>3)
- Object construction is complex
- Creating immutable objects
- Example: Building complex queries, HTML elements
Choose Prototype when:
- Object creation is expensive (database loads, complex initialization)
- You need independent copies with similar state
- Example: Cloning configuration templates
Avoid Singleton when:
- Testing is important (hard to mock)
- You need multiple instances later
- Use dependency injection instead
Modern C# Alternatives
// Instead of Singleton - use DI
services.AddSingleton<IConfigurationManager, ConfigurationManager>();
// Instead of Factory Method - use static factory methods
public static User CreateCustomer(string name) => new User { Name = name, Role = "Customer" };
// Instead of Builder - use object initializers for simple cases
var user = new User
{
Name = "John",
Email = "john@example.com",
Age = 30
};
// Instead of Prototype - use with expressions for records
var clone = original with { Name = "NewName" };
Found this guide helpful? Share it with your team:
Share on LinkedIn