Creational Design Patterns for LLD
Series: Low Level Design for .NET Developers | Previous: Chapter 2: SOLID Principles | Next: Chapter 4: Structural Design Patterns
📖 Introduction
Creational design patterns deal with object creation mechanisms. While creating objects in C# seems simple with the new keyword, complex applications require more sophisticated approaches. These patterns give you control over:
- What gets created
- Who creates it
- When it gets created
- How it gets created
In this chapter, we'll explore five essential creational patterns:
| Pattern | Purpose | When to Use |
|---|---|---|
| Singleton | Ensure only one instance exists | Configuration, logging, caching, connection pools |
| Factory Method | Define interface for creation, let subclasses decide | When class can't anticipate object types it needs |
| Abstract Factory | Create families of related objects | UI toolkits, cross-platform applications |
| Builder | Construct complex objects step by step | Objects with many optional parameters, complex construction |
| Prototype | Create objects by cloning existing ones | When object creation is expensive, need to avoid initialization cost |
1. Singleton Pattern
1.1 Understanding Singleton
Definition: The Singleton pattern ensures a class has only one instance and provides a global point of access to it.
Real-World Analogy: A country has only one government. Any citizen accesses the same government instance. If a new government was created every time someone needed it, there would be chaos!
Common Use Cases:
- Configuration managers
- Logging services
- Database connection pools
- Cache managers
- Thread pools
1.2 ❌ Non-Thread-Safe Singleton
// ❌ WARNING: Not thread-safe!
public sealed class ConfigurationManager
{
private static ConfigurationManager _instance;
private ConfigurationManager()
{
// Private constructor prevents external instantiation
Console.WriteLine("ConfigurationManager created");
LoadConfiguration();
}
public static ConfigurationManager Instance
{
get
{
if (_instance == null) // Multiple threads can enter here simultaneously
{
_instance = new ConfigurationManager();
}
return _instance;
}
}
public string DatabaseConnectionString { get; private set; }
private void LoadConfiguration()
{
// Simulate loading configuration
DatabaseConnectionString = "Server=.;Database=MyApp;Trusted_Connection=true";
Thread.Sleep(100); // Simulate work
}
}
// Problem: In multi-threaded environment, two threads can create two instances!
1.3 ✅ Thread-Safe Singleton with Lazy<T>
// ✅ Best practice: Thread-safe, lazy initialization
public sealed class ConfigurationManager
{
// Lazy<T> ensures thread safety and lazy initialization
private static readonly Lazy<ConfigurationManager> _instance =
new Lazy<ConfigurationManager>(() => new ConfigurationManager());
private ConfigurationManager()
{
Console.WriteLine("ConfigurationManager created (thread-safe)");
LoadConfiguration();
}
public static ConfigurationManager Instance => _instance.Value;
public string DatabaseConnectionString { get; private set; }
public string ApiKey { get; private set; }
public string Environment { get; private set; }
public int MaxRetryCount { get; private set; }
private void LoadConfiguration()
{
// Simulate loading from appsettings.json or environment
DatabaseConnectionString = "Server=.;Database=MyApp;Trusted_Connection=true";
ApiKey = "your-api-key-here";
Environment = "Production";
MaxRetryCount = 3;
Console.WriteLine("Configuration loaded successfully");
}
public void ReloadConfiguration()
{
Console.WriteLine("Reloading configuration...");
LoadConfiguration();
}
}
// Usage
public class ApplicationStartup
{
public void Initialize()
{
// Single instance accessed globally
var config1 = ConfigurationManager.Instance;
var config2 = ConfigurationManager.Instance;
Console.WriteLine($"Same instance? {ReferenceEquals(config1, config2)}"); // True
Console.WriteLine($"DB Connection: {config1.DatabaseConnectionString}");
Console.WriteLine($"API Key: {config1.ApiKey}");
}
}
1.4 Singleton with Double-Check Locking (Alternative)
// Alternative approach: Double-check locking (for .NET Framework compatibility)
public sealed class Logger
{
private static Logger _instance;
private static readonly object _lock = new object();
private readonly List<string> _logs;
private readonly string _logFilePath;
private Logger()
{
_logs = new List<string>();
_logFilePath = $"logs\\app_{DateTime.Now:yyyyMMdd}.log";
Directory.CreateDirectory("logs");
Console.WriteLine("Logger initialized");
}
public static Logger Instance
{
get
{
if (_instance == null) // First check (no locking)
{
lock (_lock) // Lock only if instance doesn't exist
{
if (_instance == null) // Second check
{
_instance = new Logger();
}
}
}
return _instance;
}
}
public void LogInfo(string message)
{
var logEntry = $"[INFO] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}";
_logs.Add(logEntry);
File.AppendAllText(_logFilePath, logEntry + Environment.NewLine);
Console.WriteLine(logEntry);
}
public void LogError(string message, Exception ex = null)
{
var logEntry = $"[ERROR] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}";
if (ex != null)
logEntry += $"\nException: {ex.Message}\nStack Trace: {ex.StackTrace}";
_logs.Add(logEntry);
File.AppendAllText(_logFilePath, logEntry + Environment.NewLine);
Console.WriteLine(logEntry);
}
public List<string> GetRecentLogs(int count = 10)
{
return _logs.TakeLast(count).ToList();
}
}
// Usage across multiple classes - all use the same logger instance
public class OrderService
{
public void ProcessOrder(int orderId)
{
Logger.Instance.LogInfo($"Processing order {orderId}");
// Business logic...
Logger.Instance.LogInfo($"Order {orderId} processed successfully");
}
}
public class PaymentService
{
public void ProcessPayment(decimal amount)
{
Logger.Instance.LogInfo($"Processing payment of {amount:C}");
// Payment logic...
Logger.Instance.LogInfo($"Payment of {amount:C} completed");
}
}
💡 Singleton Best Practices:
- Use
Lazy<T>for simplest thread-safe implementation - Make the class
sealedto prevent inheritance - Make constructor
privateto prevent external instantiation - Consider if you really need Singleton - sometimes static class is sufficient
- Singleton makes unit testing harder - consider dependency injection alternatives
2. Factory Method Pattern
2.1 Understanding Factory Method
Definition: The Factory Method pattern defines an interface for creating an object, but lets subclasses decide which class to instantiate. It lets a class defer instantiation to subclasses.
Real-World Analogy: A recruitment agency (creator) doesn't know which specific candidate (product) will be hired. Different departments (subclasses) create different types of candidates.
2.2 ❌ Without Factory Method - Tight Coupling
public class DocumentProcessor
{
private readonly string _documentType;
public DocumentProcessor(string documentType)
{
_documentType = documentType;
}
public void Process(string filePath)
{
// Tight coupling - directly creating specific types
if (_documentType == "PDF")
{
var parser = new PdfParser();
parser.Parse(filePath);
var validator = new PdfValidator();
validator.Validate(parser.GetData());
}
else if (_documentType == "Excel")
{
var parser = new ExcelParser();
parser.Parse(filePath);
var validator = new ExcelValidator();
validator.Validate(parser.GetData());
}
else if (_documentType == "Word")
{
var parser = new WordParser();
parser.Parse(filePath);
var validator = new WordValidator();
validator.Validate(parser.GetData());
}
// Adding new document type requires modifying this class!
}
}
2.3 ✅ Factory Method Implementation
// Product interface
public interface IDocumentParser
{
DocumentData Parse(string filePath);
string DocumentType { get; }
}
// Concrete products
public class PdfParser : IDocumentParser
{
public string DocumentType => "PDF";
public DocumentData Parse(string filePath)
{
Console.WriteLine($"Parsing PDF file: {filePath}");
// PDF-specific parsing logic using iTextSharp or PdfPig
return new DocumentData
{
Content = File.ReadAllBytes(filePath),
Metadata = new Dictionary<string, string>
{
["Pages"] = "10",
["Author"] = "John Doe"
}
};
}
}
public class ExcelParser : IDocumentParser
{
public string DocumentType => "Excel";
public DocumentData Parse(string filePath)
{
Console.WriteLine($"Parsing Excel file: {filePath}");
// Excel-specific parsing using EPPlus or ClosedXML
return new DocumentData
{
Content = File.ReadAllBytes(filePath),
Metadata = new Dictionary<string, string>
{
["Worksheets"] = "3",
["Rows"] = "1000"
}
};
}
}
public class WordParser : IDocumentParser
{
public string DocumentType => "Word";
public DocumentData Parse(string filePath)
{
Console.WriteLine($"Parsing Word file: {filePath}");
// Word-specific parsing using OpenXml or DocX
return new DocumentData
{
Content = File.ReadAllBytes(filePath),
Metadata = new Dictionary<string, string>
{
["Pages"] = "25",
["Words"] = "5000"
}
};
}
}
// Document data transfer object
public class DocumentData
{
public byte[] Content { get; set; }
public Dictionary<string, string> Metadata { get; set; }
}
// Creator abstract class
public abstract class DocumentProcessor
{
// Factory Method - subclasses override this
public abstract IDocumentParser CreateParser();
// Template method - uses the factory method
public void ProcessDocument(string filePath)
{
Console.WriteLine($"Starting document processing for: {filePath}");
var parser = CreateParser();
Console.WriteLine($"Using {parser.DocumentType} parser");
var data = parser.Parse(filePath);
ValidateDocument(data);
ExtractMetadata(data);
GeneratePreview(data);
Console.WriteLine($"Document processing completed\n");
}
protected virtual void ValidateDocument(DocumentData data)
{
Console.WriteLine("Validating document structure...");
// Common validation logic
}
protected virtual void ExtractMetadata(DocumentData data)
{
Console.WriteLine("Extracting metadata...");
foreach (var meta in data.Metadata)
{
Console.WriteLine($" {meta.Key}: {meta.Value}");
}
}
protected virtual void GeneratePreview(DocumentData data)
{
Console.WriteLine("Generating preview...");
// Common preview generation logic
}
}
// Concrete creators
public class PdfDocumentProcessor : DocumentProcessor
{
public override IDocumentParser CreateParser()
{
return new PdfParser();
}
protected override void ValidateDocument(DocumentData data)
{
base.ValidateDocument(data);
Console.WriteLine("PDF-specific validation: Checking digital signatures...");
}
}
public class ExcelDocumentProcessor : DocumentProcessor
{
public override IDocumentParser CreateParser()
{
return new ExcelParser();
}
protected override void ValidateDocument(DocumentData data)
{
base.ValidateDocument(data);
Console.WriteLine("Excel-specific validation: Checking formulas...");
}
}
public class WordDocumentProcessor : DocumentProcessor
{
public override IDocumentParser CreateParser()
{
return new WordParser();
}
}
// Usage
public class DocumentManagementSystem
{
public void ProcessDocuments()
{
// Each processor handles its specific document type
var processors = new List<DocumentProcessor>
{
new PdfDocumentProcessor(),
new ExcelDocumentProcessor(),
new WordDocumentProcessor()
};
foreach (var processor in processors)
{
processor.ProcessDocument($"sample.{processor.CreateParser().DocumentType.ToLower()}");
}
}
}
2.4 Factory Method with Dependency Injection
// Alternative: Factory Method with DI and registration
public interface IDocumentParserFactory
{
IDocumentParser CreateParser(string documentType);
}
public class DocumentParserFactory : IDocumentParserFactory
{
private readonly IServiceProvider _serviceProvider;
public DocumentParserFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IDocumentParser CreateParser(string documentType)
{
return documentType.ToLower() switch
{
"pdf" => _serviceProvider.GetRequiredService<PdfParser>(),
"excel" => _serviceProvider.GetRequiredService<ExcelParser>(),
"word" => _serviceProvider.GetRequiredService<WordParser>(),
_ => throw new NotSupportedException($"Document type {documentType} not supported")
};
}
}
// Document processor using the factory
public class FlexibleDocumentProcessor
{
private readonly IDocumentParserFactory _parserFactory;
public FlexibleDocumentProcessor(IDocumentParserFactory parserFactory)
{
_parserFactory = parserFactory;
}
public void ProcessDocument(string filePath, string documentType)
{
var parser = _parserFactory.CreateParser(documentType);
var data = parser.Parse(filePath);
Console.WriteLine($"Successfully processed {documentType} document");
// Process the data...
}
}
// DI Registration
// services.AddScoped<PdfParser>();
// services.AddScoped<ExcelParser>();
// services.AddScoped<WordParser>();
// services.AddScoped<IDocumentParserFactory, DocumentParserFactory>();
// services.AddScoped<FlexibleDocumentProcessor>();
3. Abstract Factory Pattern
3.1 Understanding Abstract Factory
Definition: The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Real-World Analogy: A furniture store (abstract factory) sells complete sets of furniture (family of products). You can buy a Modern set (concrete factory) or a Victorian set, but each set includes a chair, a table, and a sofa that match each other.
3.2 Abstract Factory Implementation - UI Theme System
// Abstract Products
public interface IButton
{
void Render();
void OnClick();
}
public interface ITextBox
{
void Render();
string GetText();
void SetText(string text);
}
public interface IWindow
{
void Render();
void Close();
void Minimize();
}
// Concrete Products - Light Theme
public class LightButton : IButton
{
public void Render()
{
Console.WriteLine("Rendering light button: white background, black text");
}
public void OnClick()
{
Console.WriteLine("Light button clicked with subtle animation");
}
}
public class LightTextBox : ITextBox
{
private string _text;
public void Render()
{
Console.WriteLine("Rendering light textbox: white background, gray border");
}
public string GetText() => _text;
public void SetText(string text)
{
_text = text;
Console.WriteLine($"Light textbox text set to: {text}");
}
}
public class LightWindow : IWindow
{
public void Render()
{
Console.WriteLine("Rendering light window: white background, light gray border");
}
public void Close()
{
Console.WriteLine("Closing light window with fade effect");
}
public void Minimize()
{
Console.WriteLine("Minimizing light window with smooth animation");
}
}
// Concrete Products - Dark Theme
public class DarkButton : IButton
{
public void Render()
{
Console.WriteLine("Rendering dark button: dark gray background, white text");
}
public void OnClick()
{
Console.WriteLine("Dark button clicked with glow effect");
}
}
public class DarkTextBox : ITextBox
{
private string _text;
public void Render()
{
Console.WriteLine("Rendering dark textbox: dark gray background, light gray border");
}
public string GetText() => _text;
public void SetText(string text)
{
_text = text;
Console.WriteLine($"Dark textbox text set to: {text}");
}
}
public class DarkWindow : IWindow
{
public void Render()
{
Console.WriteLine("Rendering dark window: dark background, subtle border");
}
public void Close()
{
Console.WriteLine("Closing dark window with fade to black effect");
}
public void Minimize()
{
Console.WriteLine("Minimizing dark window with smooth animation");
}
}
// Abstract Factory
public interface IUIThemeFactory
{
IButton CreateButton();
ITextBox CreateTextBox();
IWindow CreateWindow();
}
// Concrete Factories
public class LightThemeFactory : IUIThemeFactory
{
public IButton CreateButton() => new LightButton();
public ITextBox CreateTextBox() => new LightTextBox();
public IWindow CreateWindow() => new LightWindow();
}
public class DarkThemeFactory : IUIThemeFactory
{
public IButton CreateButton() => new DarkButton();
public ITextBox CreateTextBox() => new DarkTextBox();
public IWindow CreateWindow() => new DarkWindow();
}
// Client Application
public class Application
{
private IUIThemeFactory _themeFactory;
private IButton _button;
private ITextBox _textBox;
private IWindow _window;
public Application(IUIThemeFactory themeFactory)
{
_themeFactory = themeFactory;
}
public void InitializeUI()
{
_button = _themeFactory.CreateButton();
_textBox = _themeFactory.CreateTextBox();
_window = _themeFactory.CreateWindow();
}
public void RenderUI()
{
Console.WriteLine("\n=== Rendering UI ===");
_window.Render();
_textBox.Render();
_button.Render();
}
public void DemonstrateInteraction()
{
Console.WriteLine("\n=== User Interaction ===");
_textBox.SetText("Hello, World!");
Console.WriteLine($"Text in textbox: {_textBox.GetText()}");
_button.OnClick();
_window.Minimize();
}
}
// Usage
public class ThemeDemo
{
public static void Run()
{
// Use Light Theme
Console.WriteLine("=== LIGHT THEME ===");
IUIThemeFactory lightFactory = new LightThemeFactory();
var app = new Application(lightFactory);
app.InitializeUI();
app.RenderUI();
app.DemonstrateInteraction();
// Switch to Dark Theme - just change the factory!
Console.WriteLine("\n\n=== DARK THEME ===");
IUIThemeFactory darkFactory = new DarkThemeFactory();
app = new Application(darkFactory);
app.InitializeUI();
app.RenderUI();
app.DemonstrateInteraction();
}
}
3.3 Abstract Factory with Configuration
// Theme configuration
public enum Theme
{
Light,
Dark,
Blue, // New theme - just add new factory!
HighContrast
}
public class ThemeFactoryProvider
{
private readonly Dictionary<Theme, IUIThemeFactory> _factories;
public ThemeFactoryProvider()
{
_factories = new Dictionary<Theme, IUIThemeFactory>
{
[Theme.Light] = new LightThemeFactory(),
[Theme.Dark] = new DarkThemeFactory(),
// [Theme.Blue] = new BlueThemeFactory(), // Add new theme without changing client code
};
}
public IUIThemeFactory GetFactory(Theme theme)
{
if (_factories.TryGetValue(theme, out var factory))
return factory;
throw new NotSupportedException($"Theme {theme} not supported");
}
}
// Dynamic theme switching
public class ThemeAwareApplication
{
private IUIThemeFactory _currentTheme;
private readonly ThemeFactoryProvider _factoryProvider;
public ThemeAwareApplication(ThemeFactoryProvider factoryProvider)
{
_factoryProvider = factoryProvider;
_currentTheme = _factoryProvider.GetFactory(Theme.Light); // Default
}
public void SwitchTheme(Theme theme)
{
Console.WriteLine($"\n=== Switching to {theme} Theme ===");
_currentTheme = _factoryProvider.GetFactory(theme);
ReinitializeUI();
}
private void ReinitializeUI()
{
// Recreate UI components with new theme
var button = _currentTheme.CreateButton();
var textBox = _currentTheme.CreateTextBox();
var window = _currentTheme.CreateWindow();
button.Render();
textBox.Render();
window.Render();
}
}
4. Builder Pattern
4.1 Understanding Builder
Definition: The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
Real-World Analogy: Building a computer. You have the same construction process (assemble CPU, add RAM, install storage, etc.), but you can create different configurations (gaming PC, office PC, server).
4.2 ❌ Without Builder - Telescoping Constructor Anti-Pattern
public class Email
{
public string To { get; set; }
public string From { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public List<string> Cc { get; set; }
public List<string> Bcc { get; set; }
public List<string> Attachments { get; set; }
public bool IsHtml { get; set; }
public Priority Priority { get; set; }
public DateTime? SendOn { get; set; }
public bool IsReadReceiptRequested { get; set; }
// Telescoping constructor - many combinations!
public Email(string to, string from) { }
public Email(string to, string from, string subject) { }
public Email(string to, string from, string subject, string body) { }
// Need many more constructors for all combinations! This is unmaintainable.
}
4.3 ✅ Builder Pattern Implementation
// Product class
public class Email
{
// Required properties
public string To { get; private set; }
public string From { get; private set; }
// Optional properties with defaults
public string Subject { get; private set; } = "";
public string Body { get; private set; } = "";
public List<string> Cc { get; private set; } = new List<string>();
public List<string> Bcc { get; private set; } = new List<string>();
public List<string> Attachments { get; private set; } = new List<string>();
public bool IsHtml { get; private set; } = false;
public Priority Priority { get; private set; } = Priority.Normal;
public DateTime? SendOn { get; private set; } = null;
public bool IsReadReceiptRequested { get; private set; } = false;
// Private constructor - only Builder can create
private Email() { }
// Nested Builder class
public class Builder
{
private readonly Email _email;
public Builder(string to, string from)
{
_email = new Email();
_email.To = to ?? throw new ArgumentNullException(nameof(to));
_email.From = from ?? throw new ArgumentNullException(nameof(from));
}
public Builder WithSubject(string subject)
{
_email.Subject = subject ?? "";
return this;
}
public Builder WithBody(string body, bool isHtml = false)
{
_email.Body = body ?? "";
_email.IsHtml = isHtml;
return this;
}
public Builder AddCc(params string[] emails)
{
_email.Cc.AddRange(emails);
return this;
}
public Builder AddBcc(params string[] emails)
{
_email.Bcc.AddRange(emails);
return this;
}
public Builder AddAttachment(string filePath)
{
if (!File.Exists(filePath))
throw new FileNotFoundException($"Attachment not found: {filePath}");
_email.Attachments.Add(filePath);
return this;
}
public Builder WithPriority(Priority priority)
{
_email.Priority = priority;
return this;
}
public Builder ScheduleSend(DateTime sendOn)
{
if (sendOn < DateTime.Now)
throw new ArgumentException("Send date must be in the future");
_email.SendOn = sendOn;
return this;
}
public Builder RequestReadReceipt()
{
_email.IsReadReceiptRequested = true;
return this;
}
public Email Build()
{
// Validation before building
if (string.IsNullOrEmpty(_email.To))
throw new InvalidOperationException("Email must have a recipient");
if (string.IsNullOrEmpty(_email.From))
throw new InvalidOperationException("Email must have a sender");
if (string.IsNullOrEmpty(_email.Body) && string.IsNullOrEmpty(_email.Subject))
throw new InvalidOperationException("Email must have either subject or body");
return _email;
}
}
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"To: {To}");
sb.AppendLine($"From: {From}");
if (Cc.Any()) sb.AppendLine($"Cc: {string.Join(", ", Cc)}");
if (Bcc.Any()) sb.AppendLine($"Bcc: {string.Join(", ", Bcc)}");
if (!string.IsNullOrEmpty(Subject)) sb.AppendLine($"Subject: {Subject}");
sb.AppendLine($"Body: {(IsHtml ? "HTML" : "Text")}");
if (Attachments.Any()) sb.AppendLine($"Attachments: {string.Join(", ", Attachments)}");
sb.AppendLine($"Priority: {Priority}");
if (SendOn.HasValue) sb.AppendLine($"Scheduled: {SendOn.Value}");
if (IsReadReceiptRequested) sb.AppendLine("Read receipt requested");
return sb.ToString();
}
}
public enum Priority
{
Low,
Normal,
High,
Urgent
}
// Usage - Fluent interface
public class EmailService
{
public void SendEmail()
{
// Simple email
var simpleEmail = new Email.Builder("user@example.com", "noreply@company.com")
.WithSubject("Welcome!")
.WithBody("Thank you for signing up!", isHtml: false)
.Build();
Console.WriteLine("Simple Email:");
Console.WriteLine(simpleEmail);
// Complex email with all options
var complexEmail = new Email.Builder("customer@example.com", "support@company.com")
.WithSubject("Your Order #12345")
.WithBody("<h1>Order Confirmation</h1><p>Your order has been shipped!</p>", isHtml: true)
.AddCc("manager@company.com", "warehouse@company.com")
.AddBcc("audit@company.com")
.AddAttachment(@"C:\invoices\invoice_12345.pdf")
.AddAttachment(@"C:\receipts\receipt_12345.pdf")
.WithPriority(Priority.High)
.ScheduleSend(DateTime.Now.AddHours(2))
.RequestReadReceipt()
.Build();
Console.WriteLine("\nComplex Email:");
Console.WriteLine(complexEmail);
}
}
4.4 Director - Reusing Construction Processes
// Director class - defines reusable construction processes
public class EmailDirector
{
public Email BuildWelcomeEmail(string email, string userName)
{
return new Email.Builder(email, "welcome@company.com")
.WithSubject($"Welcome to Our Platform, {userName}!")
.WithBody($@"
Welcome {userName}!
Thank you for joining our platform. We're excited to have you!
Get started by completing your profile and exploring our features.
Best regards,
The Team
", isHtml: true)
.Build();
}
public Email BuildPasswordResetEmail(string email, string resetToken)
{
return new Email.Builder(email, "security@company.com")
.WithSubject("Password Reset Request")
.WithBody($@"
Password Reset
We received a request to reset your password.
Click here to reset your password.
If you didn't request this, please ignore this email.
", isHtml: true)
.WithPriority(Priority.High)
.Build();
}
public Email BuildOrderConfirmation(string email, int orderId, List<OrderItem> items)
{
var body = new StringBuilder();
body.AppendLine($"<h1>Order #{orderId} Confirmed!</h1>");
body.AppendLine("<table border='1'>");
body.AppendLine("<tr><th>Product</th><th>Quantity</th><th>Price</th></tr>");
foreach (var item in items)
{
body.AppendLine($"<tr><td>{item.ProductName}</td><td>{item.Quantity}</td><td>{item.Price:C}</td></tr>");
}
body.AppendLine("</table>");
return new Email.Builder(email, "orders@company.com")
.WithSubject($"Order #{orderId} Confirmation")
.WithBody(body.ToString(), isHtml: true)
.AddAttachment($"invoices/invoice_{orderId}.pdf")
.Build();
}
}
// Usage with Director
public class AutomatedEmailSystem
{
private readonly EmailDirector _director;
public AutomatedEmailSystem()
{
_director = new EmailDirector();
}
public async Task SendWelcomeEmailAsync(string email, string userName)
{
var welcomeEmail = _director.BuildWelcomeEmail(email, userName);
await SendAsync(welcomeEmail);
Console.WriteLine($"Welcome email sent to {email}");
}
public async Task SendPasswordResetAsync(string email, string resetToken)
{
var resetEmail = _director.BuildPasswordResetEmail(email, resetToken);
await SendAsync(resetEmail);
Console.WriteLine($"Password reset email sent to {email}");
}
private Task SendAsync(Email email)
{
// Actual email sending logic
Console.WriteLine($"Sending email:\n{email}");
return Task.CompletedTask;
}
}
💡 Builder Pattern Benefits:
- Fluent interface: Readable, expressive code
- Immutability: Product can be immutable after construction
- Validation: All validation happens in Build() method
- Reusable construction processes: Director can define standard configurations
- Avoid telescoping constructors: No more 10-parameter constructors
5. Prototype Pattern
5.1 Understanding Prototype
Definition: The Prototype pattern creates new objects by cloning existing instances rather than calling constructors. This is useful when object creation is expensive or complex.
Real-World Analogy: Cell division - cells replicate by cloning themselves rather than building from scratch each time.
5.2 Prototype Implementation with ICloneable
// Product class with cloning support
public class Product : ICloneable
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
public List<string> Tags { get; set; }
public Dictionary<string, string> Attributes { get; set; }
public DateTime CreatedAt { get; set; }
public Product()
{
Tags = new List<string>();
Attributes = new Dictionary<string, string>();
CreatedAt = DateTime.Now;
}
// Shallow clone - copies value types and references
public object Clone()
{
return this.MemberwiseClone();
}
// Deep clone - creates new instances of reference types
public Product DeepClone()
{
var cloned = (Product)this.MemberwiseClone();
cloned.Tags = new List<string>(this.Tags);
cloned.Attributes = new Dictionary<string, string>(this.Attributes);
return cloned;
}
public override string ToString()
{
return $"Product: {Name} (${Price}) - Tags: {string.Join(", ", Tags)}";
}
}
// Document template with cloning
public class DocumentTemplate : ICloneable
{
public string Name { get; set; }
public string Content { get; set; }
public Dictionary<string, string> Placeholders { get; set; }
public List<string> Sections { get; set; }
public DocumentStyle Style { get; set; }
public DocumentTemplate()
{
Placeholders = new Dictionary<string, string>();
Sections = new List<string>();
Style = new DocumentStyle();
}
public object Clone()
{
var cloned = (DocumentTemplate)this.MemberwiseClone();
cloned.Placeholders = new Dictionary<string, string>(this.Placeholders);
cloned.Sections = new List<string>(this.Sections);
cloned.Style = (DocumentStyle)this.Style.Clone();
return cloned;
}
}
public class DocumentStyle : ICloneable
{
public string FontFamily { get; set; } = "Arial";
public int FontSize { get; set; } = 12;
public string Color { get; set; } = "Black";
public bool IsBold { get; set; } = false;
public bool IsItalic { get; set; } = false;
public object Clone()
{
return this.MemberwiseClone();
}
}
// Prototype Registry - stores and manages prototypes
public class DocumentTemplateRegistry
{
private readonly Dictionary<string, DocumentTemplate> _templates = new();
public void RegisterTemplate(string key, DocumentTemplate template)
{
_templates[key] = template;
}
public DocumentTemplate GetTemplate(string key)
{
if (_templates.TryGetValue(key, out var template))
{
return (DocumentTemplate)template.Clone(); // Return a clone
}
throw new KeyNotFoundException($"Template '{key}' not found");
}
public void InitializeTemplates()
{
// Invoice template
var invoiceTemplate = new DocumentTemplate
{
Name = "Invoice",
Sections = new List<string> { "Header", "Company Details", "Customer Details", "Items", "Total", "Footer" },
Placeholders = new Dictionary<string, string>
{
["{InvoiceNumber}"] = "",
["{Date}"] = "",
["{CustomerName}"] = "",
["{Total}"] = ""
}
};
RegisterTemplate("invoice", invoiceTemplate);
// Report template
var reportTemplate = new DocumentTemplate
{
Name = "Report",
Sections = new List<string> { "Title", "Executive Summary", "Data", "Analysis", "Conclusion" },
Placeholders = new Dictionary<string, string>
{
["{ReportTitle}"] = "",
["{Author}"] = "",
["{Date}"] = "",
["{Summary}"] = ""
},
Style = new DocumentStyle { FontFamily = "Times New Roman", FontSize = 11 }
};
RegisterTemplate("report", reportTemplate);
// Letter template
var letterTemplate = new DocumentTemplate
{
Name = "Letter",
Sections = new List<string> { "Sender", "Date", "Recipient", "Subject", "Body", "Closing" },
Placeholders = new Dictionary<string, string>
{
["{SenderName}"] = "",
["{RecipientName}"] = "",
["{Subject}"] = "",
["{Body}"] = ""
},
Style = new DocumentStyle { FontFamily = "Georgia", FontSize = 12, IsItalic = false }
};
RegisterTemplate("letter", letterTemplate);
}
}
// Usage
public class DocumentGenerationSystem
{
private readonly DocumentTemplateRegistry _registry;
public DocumentGenerationSystem()
{
_registry = new DocumentTemplateRegistry();
_registry.InitializeTemplates();
}
public void GenerateInvoice(string customerName, decimal total)
{
var invoice = _registry.GetTemplate("invoice");
// Customize the cloned template
invoice.Placeholders["{InvoiceNumber}"] = $"INV-{DateTime.Now:yyyyMMdd}-{new Random().Next(1000, 9999)}";
invoice.Placeholders["{Date}"] = DateTime.Now.ToShortDateString();
invoice.Placeholders["{CustomerName}"] = customerName;
invoice.Placeholders["{Total}"] = total.ToString("C");
Console.WriteLine($"\n=== Generated {invoice.Name} ===");
Console.WriteLine($"Sections: {string.Join(" -> ", invoice.Sections)}");
Console.WriteLine($"Style: {invoice.Style.FontFamily}, {invoice.Style.FontSize}pt");
foreach (var placeholder in invoice.Placeholders)
{
Console.WriteLine($"{placeholder.Key} = {placeholder.Value}");
}
}
public void GenerateReport(string title, string author)
{
var report = _registry.GetTemplate("report");
report.Placeholders["{ReportTitle}"] = title;
report.Placeholders["{Author}"] = author;
report.Placeholders["{Date}"] = DateTime.Now.ToShortDateString();
report.Placeholders["{Summary}"] = "This is a generated summary...";
Console.WriteLine($"\n=== Generated {report.Name} ===");
Console.WriteLine($"Title: {report.Placeholders["{ReportTitle}"]}");
Console.WriteLine($"Author: {author}");
Console.WriteLine($"Style: {report.Style.FontFamily}, {report.Style.FontSize}pt");
}
}
// Performance comparison - Constructor vs Prototype
public class PerformanceComparison
{
public void Compare()
{
// Expensive object creation (simulated)
Console.WriteLine("Creating complex object via constructor...");
var sw = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < 10000; i++)
{
var product = new Product
{
Id = i,
Name = "Product " + i,
Price = 99.99m,
Category = "Electronics",
Tags = new List<string> { "new", "sale", "popular" },
Attributes = new Dictionary<string, string>
{
["color"] = "red",
["size"] = "large",
["material"] = "plastic"
}
};
}
sw.Stop();
Console.WriteLine($"Constructor: {sw.ElapsedMilliseconds}ms");
// Create a prototype
var prototype = new Product
{
Id = 0,
Name = "Template",
Price = 99.99m,
Category = "Electronics",
Tags = new List<string> { "new", "sale", "popular" },
Attributes = new Dictionary<string, string>
{
["color"] = "red",
["size"] = "large",
["material"] = "plastic"
}
};
Console.WriteLine("Creating complex object via cloning...");
sw.Restart();
for (int i = 0; i < 10000; i++)
{
var product = prototype.DeepClone();
product.Id = i;
product.Name = "Product " + i;
}
sw.Stop();
Console.WriteLine($"Prototype: {sw.ElapsedMilliseconds}ms");
}
}
6. Choosing the Right Creational Pattern
| Scenario | Recommended Pattern | Reason |
|---|---|---|
| Need exactly one instance of a class | Singleton | Ensures single instance with global access |
| Creating objects based on input/configuration | Factory Method | Encapsulates creation logic, easy to extend |
| Creating families of related objects | Abstract Factory | Ensures compatibility between created objects |
| Objects with many optional parameters | Builder | Fluent interface, avoids telescoping constructors |
| Expensive object creation, many similar instances | Prototype | Clone instead of recreating from scratch |
| Need to hide creation complexity | Factory Method or Abstract Factory | Encapsulates complex creation logic |
| Runtime configuration of object creation | Abstract Factory | Can swap factories at runtime |
7. Real-World Example: E-Commerce Order System
Combining multiple creational patterns in a real-world scenario:
// ===== Singleton: Order Number Generator =====
public sealed class OrderNumberGenerator
{
private static readonly Lazy<OrderNumberGenerator> _instance =
new Lazy<OrderNumberGenerator>(() => new OrderNumberGenerator());
private int _currentNumber;
private readonly object _lock = new object();
private OrderNumberGenerator()
{
_currentNumber = DateTime.Now.Year * 10000;
}
public static OrderNumberGenerator Instance => _instance.Value;
public string GenerateOrderNumber()
{
lock (_lock)
{
_currentNumber++;
return $"ORD-{_currentNumber}";
}
}
}
// ===== Abstract Factory: Payment Processing =====
public interface IPaymentProcessor
{
Task<PaymentResult> ProcessPaymentAsync(decimal amount);
}
public interface IRefundProcessor
{
Task<RefundResult> ProcessRefundAsync(string transactionId, decimal amount);
}
public interface IPaymentFactory
{
IPaymentProcessor CreatePaymentProcessor();
IRefundProcessor CreateRefundProcessor();
}
public class StripePaymentFactory : IPaymentFactory
{
private readonly string _apiKey;
public StripePaymentFactory(string apiKey)
{
_apiKey = apiKey;
}
public IPaymentProcessor CreatePaymentProcessor() => new StripePaymentProcessor(_apiKey);
public IRefundProcessor CreateRefundProcessor() => new StripeRefundProcessor(_apiKey);
}
public class PayPalPaymentFactory : IPaymentFactory
{
private readonly string _clientId;
private readonly string _clientSecret;
public PayPalPaymentFactory(string clientId, string clientSecret)
{
_clientId = clientId;
_clientSecret = clientSecret;
}
public IPaymentProcessor CreatePaymentProcessor() => new PayPalPaymentProcessor(_clientId, _clientSecret);
public IRefundProcessor CreateRefundProcessor() => new PayPalRefundProcessor(_clientId, _clientSecret);
}
// Concrete implementations (simplified)
public class StripePaymentProcessor : IPaymentProcessor
{
public StripePaymentProcessor(string apiKey) { }
public Task<PaymentResult> ProcessPaymentAsync(decimal amount) => Task.FromResult(PaymentResult.Success());
}
public class StripeRefundProcessor : IRefundProcessor
{
public StripeRefundProcessor(string apiKey) { }
public Task<RefundResult> ProcessRefundAsync(string transactionId, decimal amount) => Task.FromResult(RefundResult.Success());
}
public class PayPalPaymentProcessor : IPaymentProcessor
{
public PayPalPaymentProcessor(string clientId, string clientSecret) { }
public Task<PaymentResult> ProcessPaymentAsync(decimal amount) => Task.FromResult(PaymentResult.Success());
}
public class PayPalRefundProcessor : IRefundProcessor
{
public PayPalRefundProcessor(string clientId, string clientSecret) { }
public Task<RefundResult> ProcessRefundAsync(string transactionId, decimal amount) => Task.FromResult(RefundResult.Success());
}
// ===== Builder: Order Construction =====
public class Order
{
public string OrderNumber { get; private set; }
public DateTime OrderDate { get; private set; }
public Customer Customer { get; private set; }
public List<OrderItem> Items { get; private set; }
public ShippingAddress ShippingAddress { get; private set; }
public PaymentMethod PaymentMethod { get; private set; }
public decimal Subtotal { get; private set; }
public decimal Tax { get; private set; }
public decimal ShippingCost { get; private set; }
public decimal Discount { get; private set; }
public decimal Total { get; private set; }
public string Notes { get; private set; }
public OrderStatus Status { get; private set; }
private Order() { }
public class Builder
{
private readonly Order _order;
public Builder(Customer customer)
{
_order = new Order();
_order.OrderNumber = OrderNumberGenerator.Instance.GenerateOrderNumber();
_order.OrderDate = DateTime.Now;
_order.Customer = customer;
_order.Items = new List<OrderItem>();
_order.Status = OrderStatus.Pending;
}
public Builder AddItem(Product product, int quantity)
{
var item = new OrderItem
{
ProductId = product.Id,
ProductName = product.Name,
Quantity = quantity,
UnitPrice = product.Price
};
_order.Items.Add(item);
return this;
}
public Builder AddShippingAddress(string street, string city, string state, string zipCode, string country)
{
_order.ShippingAddress = new ShippingAddress
{
Street = street,
City = city,
State = state,
ZipCode = zipCode,
Country = country
};
return this;
}
public Builder SetPaymentMethod(PaymentMethod method)
{
_order.PaymentMethod = method;
return this;
}
public Builder ApplyDiscount(decimal discountPercentage)
{
_order.Discount = _order.Subtotal * (discountPercentage / 100);
return this;
}
public Builder ApplyPromoCode(string promoCode, decimal discountAmount)
{
_order.Discount = discountAmount;
_order.Notes = $"Promo code applied: {promoCode}";
return this;
}
public Builder AddNotes(string notes)
{
_order.Notes = notes;
return this;
}
public Order Build()
{
// Calculate totals
_order.Subtotal = _order.Items.Sum(i => i.UnitPrice * i.Quantity);
_order.Tax = _order.Subtotal * 0.1m; // 10% tax
_order.ShippingCost = CalculateShippingCost(_order.Items.Sum(i => i.Quantity));
_order.Total = _order.Subtotal + _order.Tax + _order.ShippingCost - _order.Discount;
// Validate
if (_order.Items.Count == 0)
throw new InvalidOperationException("Order must have at least one item");
if (_order.ShippingAddress == null)
throw new InvalidOperationException("Shipping address is required");
return _order;
}
private decimal CalculateShippingCost(int totalItems)
{
if (totalItems > 10) return 0; // Free shipping
if (totalItems > 5) return 5.99m;
return 9.99m;
}
}
}
// ===== Factory Method: Notification Service =====
public interface INotification
{
Task SendAsync(string recipient, string message);
}
public class EmailNotification : INotification
{
public async Task SendAsync(string recipient, string message)
{
Console.WriteLine($"Sending email to {recipient}: {message}");
await Task.Delay(100);
}
}
public class SmsNotification : INotification
{
public async Task SendAsync(string recipient, string message)
{
Console.WriteLine($"Sending SMS to {recipient}: {message}");
await Task.Delay(50);
}
}
public abstract class NotificationService
{
public abstract INotification CreateNotification();
public async Task NotifyAsync(string recipient, string message)
{
var notification = CreateNotification();
await notification.SendAsync(recipient, message);
Console.WriteLine($"Notification sent via {notification.GetType().Name}");
}
}
public class EmailNotificationService : NotificationService
{
public override INotification CreateNotification() => new EmailNotification();
}
public class SmsNotificationService : NotificationService
{
public override INotification CreateNotification() => new SmsNotification();
}
// ===== Complete Order Processing System =====
public class OrderProcessingSystem
{
private readonly IPaymentFactory _paymentFactory;
private readonly NotificationService _notificationService;
public OrderProcessingSystem(IPaymentFactory paymentFactory, NotificationService notificationService)
{
_paymentFactory = paymentFactory;
_notificationService = notificationService;
}
public async Task<Order> ProcessOrderAsync(Order order)
{
Console.WriteLine($"\n=== Processing Order {order.OrderNumber} ===");
// Process payment
var paymentProcessor = _paymentFactory.CreatePaymentProcessor();
var paymentResult = await paymentProcessor.ProcessPaymentAsync(order.Total);
if (!paymentResult.IsSuccess)
{
throw new Exception("Payment failed");
}
// Update order status
// order.Status = OrderStatus.Paid;
// Send notification
await _notificationService.NotifyAsync(order.Customer.Email,
$"Your order {order.OrderNumber} has been confirmed. Total: {order.Total:C}");
Console.WriteLine($"Order {order.OrderNumber} processed successfully!");
return order;
}
}
// Usage
public class ECommerceDemo
{
public static async Task RunAsync()
{
// Build an order using Builder pattern
var customer = new Customer { Id = 1, Name = "John Doe", Email = "john@example.com" };
var order = new Order.Builder(customer)
.AddItem(new Product { Id = 1, Name = "Laptop", Price = 999.99m }, 1)
.AddItem(new Product { Id = 2, Name = "Mouse", Price = 29.99m }, 2)
.AddShippingAddress("123 Main St", "Springfield", "IL", "62701", "USA")
.SetPaymentMethod(PaymentMethod.CreditCard)
.ApplyPromoCode("SAVE20", 20)
.AddNotes("Please deliver before 5 PM")
.Build();
// Create payment factory based on configuration (Abstract Factory)
IPaymentFactory paymentFactory;
var paymentProvider = "Stripe"; // From configuration
if (paymentProvider == "Stripe")
paymentFactory = new StripePaymentFactory("sk_test_123");
else
paymentFactory = new PayPalPaymentFactory("client_id", "client_secret");
// Create notification service (Factory Method)
NotificationService notificationService = new EmailNotificationService();
// Process order
var orderSystem = new OrderProcessingSystem(paymentFactory, notificationService);
await orderSystem.ProcessOrderAsync(order);
}
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class OrderItem
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
public class ShippingAddress
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string Country { get; set; }
}
public enum PaymentMethod
{
CreditCard,
PayPal,
Crypto
}
public enum OrderStatus
{
Pending,
Paid,
Shipped,
Delivered,
Cancelled
}
public class PaymentResult
{
public bool IsSuccess { get; set; }
public static PaymentResult Success() => new PaymentResult { IsSuccess = true };
public static PaymentResult Failed(string error) => new PaymentResult { IsSuccess = false };
}
public class RefundResult
{
public bool IsSuccess { get; set; }
public static RefundResult Success() => new RefundResult { IsSuccess = true };
}
8. Summary and Key Takeaways
- Singleton: One instance, global access. Use
Lazy<T>for thread safety. - Factory Method: Let subclasses decide which objects to create. Great for frameworks and libraries.
- Abstract Factory: Create families of related objects. Perfect for cross-platform applications.
- Builder: Construct complex objects step by step. Fluent interface makes code readable.
- Prototype: Clone objects instead of creating from scratch. Ideal for expensive initialization.
🎯 When to Use Which Pattern:
- Singleton: Configuration, logging, caching, connection pools
- Factory Method: Document parsers, database providers, logger factories
- Abstract Factory: UI themes, cross-platform libraries, database families
- Builder: Email construction, SQL queries, complex DTOs, HTTP requests
- Prototype: Document templates, expensive database queries, cached objects
9. Practice Exercises
Exercise 1: Implement a Cache Manager (Singleton)
Create a thread-safe cache manager with expiration policies.
Click to see solution
public sealed class CacheManager
{
private static readonly Lazy<CacheManager> _instance = new(() => new CacheManager());
private readonly ConcurrentDictionary<string, (object Value, DateTime Expiry)> _cache;
private readonly Timer _cleanupTimer;
private CacheManager()
{
_cache = new ConcurrentDictionary<string, (object, DateTime)>();
_cleanupTimer = new Timer(CleanupExpired, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
public static CacheManager Instance => _instance.Value;
public void Set(string key, object value, TimeSpan? expiry = null)
{
var expiryTime = expiry.HasValue ? DateTime.Now.Add(expiry.Value) : DateTime.Now.AddHours(1);
_cache[key] = (value, expiryTime);
}
public T Get<T>(string key)
{
if (_cache.TryGetValue(key, out var entry))
{
if (entry.Expiry > DateTime.Now)
return (T)entry.Value;
else
_cache.TryRemove(key, out _);
}
return default;
}
private void CleanupExpired(object state)
{
foreach (var key in _cache.Keys)
{
if (_cache.TryGetValue(key, out var entry) && entry.Expiry <= DateTime.Now)
_cache.TryRemove(key, out _);
}
}
}
Exercise 2: Create a Database Connection Factory
Implement a factory that creates different database connections based on configuration.
Click to see solution
public interface IDatabaseConnection
{
void Connect();
void Disconnect();
void ExecuteQuery(string query);
}
public class SqlServerConnection : IDatabaseConnection { /* implementation */ }
public class MySqlConnection : IDatabaseConnection { /* implementation */ }
public class PostgreSqlConnection : IDatabaseConnection { /* implementation */ }
public interface IDatabaseFactory
{
IDatabaseConnection CreateConnection();
}
public class SqlServerFactory : IDatabaseFactory
{
private readonly string _connectionString;
public SqlServerFactory(string connectionString) => _connectionString = connectionString;
public IDatabaseConnection CreateConnection() => new SqlServerConnection(_connectionString);
}
public class MySqlFactory : IDatabaseFactory
{
private readonly string _connectionString;
public MySqlFactory(string connectionString) => _connectionString = connectionString;
public IDatabaseConnection CreateConnection() => new MySqlConnection(_connectionString);
}
10. What's Next?
In Chapter 4, we'll explore Structural Design Patterns - Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy. These patterns help you compose objects and classes into larger structures while keeping them flexible and efficient.
Chapter 4 Preview:
- Adapter - Make incompatible interfaces work together
- Decorator - Add responsibilities dynamically
- Facade - Simplify complex subsystems
- Proxy - Control access to objects
- Composite - Treat individual objects and compositions uniformly
📝 Practice Assignments:
- Implement a thread-safe logging service using Singleton pattern
- Create a document converter system using Factory Method (PDF, Word, Excel converters)
- Build a cross-platform UI framework using Abstract Factory
- Create a SQL query builder using Builder pattern with fluent interface
- Implement a prototype registry for report templates
- Combine multiple patterns to build a complete e-commerce checkout system
Happy Coding! 🚀