Skip to main content

9.2 - Creational Patterns

Creational design patterns abstract the instantiation process. They help make a system independent of how its objects are created, composed, and represented. These patterns become important as systems evolve to depend more on object composition than class inheritance.

9.2.1 - Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides a global point of access to it. This is useful when exactly one object is needed to coordinate actions across the system.

Basic Implementation

/// <summary>
/// Singleton class that ensures only one instance exists throughout the application
/// This implementation uses double-check locking for thread safety
/// </summary>
/// <remarks>
/// The 'sealed' keyword prevents inheritance, which could break the singleton pattern
/// </remarks>
public sealed class Singleton
{
// Static field to hold the single instance
// This is initially null until the first time the Instance property is accessed
private static Singleton _instance;

// Object used for locking to ensure thread safety
// readonly ensures the reference cannot be changed after initialization
private static readonly object _lock = new object();

// Private constructor prevents external instantiation
// This is crucial for the Singleton pattern - it forces use of the Instance property
private Singleton()
{
// Initialization code would go here
// This runs only once when the singleton is first created
}

/// <summary>
/// Gets the singleton instance, creating it if it doesn't exist
/// </summary>
/// <remarks>
/// This property implements double-check locking for thread safety and performance
/// </remarks>
public static Singleton Instance
{
get
{
// First check (without locking) for performance
// Most of the time, the instance will already exist
if (_instance == null)
{
// Lock to ensure only one thread can create the instance
// This prevents race conditions in multi-threaded environments
lock (_lock)
{
// Second check (with locking) to ensure another thread hasn't created the instance
// This is the "double-check" part of double-check locking
if (_instance == null)
{
// Create the singleton instance
_instance = new Singleton();
}
}
}
return _instance;
}
}

/// <summary>
/// Example method demonstrating that the singleton can contain business logic
/// </summary>
public void SomeBusinessLogic()
{
// Business logic implementation would go here
Console.WriteLine("Singleton business logic executed");
}
}

// USAGE EXAMPLE:
// Singleton instance1 = Singleton.Instance;
// Singleton instance2 = Singleton.Instance;
//
// // Both variables refer to the same instance
// bool areSame = ReferenceEquals(instance1, instance2); // Returns true
//
// // Use the singleton
// instance1.SomeBusinessLogic();

Thread-Safe Implementation with Lazy<T>

/// <summary>
/// Singleton class implemented using Lazy<T> for thread-safe lazy initialization
/// This is the recommended approach for implementing Singleton in modern C#
/// </summary>
public sealed class Singleton
{
// Lazy<T> provides built-in thread safety and deferred initialization
// The singleton instance won't be created until the first time it's accessed
private static readonly Lazy<Singleton> _lazy =
new Lazy<Singleton>(() => new Singleton(), LazyThreadSafetyMode.ExecutionAndPublication);

// Private constructor prevents external instantiation
private Singleton()
{
// Initialization code would go here
Console.WriteLine("Singleton instance created");
}

/// <summary>
/// Gets the singleton instance, creating it if it doesn't exist
/// </summary>
/// <remarks>
/// This property uses Lazy<T> to ensure thread-safe initialization
/// The instance is only created when this property is first accessed
/// </remarks>
public static Singleton Instance => _lazy.Value;

/// <summary>
/// Example method demonstrating that the singleton can contain business logic
/// </summary>
public void SomeBusinessLogic()
{
// Business logic implementation would go here
Console.WriteLine("Singleton business logic executed");
}
}

// USAGE EXAMPLE:
// // The instance isn't created until this line executes
// Singleton instance = Singleton.Instance;
//
// // Use the singleton
// instance.SomeBusinessLogic();
//
// // Benefits of this approach:
// // 1. Thread-safe without explicit locking code
// // 2. Lazy initialization - instance only created when needed
// // 3. Better performance than double-check locking in most scenarios
// // 4. Cleaner, more readable code

When to Use

  • When a class must have exactly one instance available to all clients
  • When the sole instance should be extensible by subclassing
  • Examples: database connections, logging, caching, thread pools, configuration managers

Considerations

  • Singletons can make unit testing difficult due to hidden dependencies
  • They can violate the Single Responsibility Principle by managing their own lifecycle
  • In multi-threaded environments, special care must be taken for thread safety

9.2.2 - Factory Method Pattern

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.

Basic Implementation

// Product interface
public interface IProduct
{
string Operation();
}

// Concrete products
public class ConcreteProductA : IProduct
{
public string Operation() => "Result of ConcreteProductA";
}

public class ConcreteProductB : IProduct
{
public string Operation() => "Result of ConcreteProductB";
}

// Creator abstract class
public abstract class Creator
{
// Factory method
public abstract IProduct FactoryMethod();

// Business logic that uses the factory method
public string SomeOperation()
{
var product = FactoryMethod();
return $"Creator: The same creator's code has just worked with {product.Operation()}";
}
}

// Concrete creators
public class ConcreteCreatorA : Creator
{
public override IProduct FactoryMethod() => new ConcreteProductA();
}

public class ConcreteCreatorB : Creator
{
public override IProduct FactoryMethod() => new ConcreteProductB();
}

Real-World Example

// Document interface and concrete implementations
public interface IDocument
{
void Open();
void Save();
}

public class PdfDocument : IDocument
{
public void Open() => Console.WriteLine("Opening PDF document");
public void Save() => Console.WriteLine("Saving PDF document");
}

public class WordDocument : IDocument
{
public void Open() => Console.WriteLine("Opening Word document");
public void Save() => Console.WriteLine("Saving Word document");
}

// Document creator abstract class
public abstract class DocumentCreator
{
public abstract IDocument CreateDocument();

public void OpenAndSaveDocument()
{
var document = CreateDocument();
document.Open();
document.Save();
}
}

// Concrete document creators
public class PdfDocumentCreator : DocumentCreator
{
public override IDocument CreateDocument() => new PdfDocument();
}

public class WordDocumentCreator : DocumentCreator
{
public override IDocument CreateDocument() => new WordDocument();
}

When to Use

  • When a class can't anticipate the class of objects it must create
  • When a class wants its subclasses to specify the objects it creates
  • When classes delegate responsibility to one of several helper subclasses, and you want to localize the knowledge of which helper subclass is the delegate

9.2.3 - Abstract Factory Pattern

The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.

Basic Implementation

// Abstract products
public interface IButton
{
void Render();
void OnClick();
}

public interface ICheckbox
{
void Render();
void OnCheck();
}

// Concrete products for Windows
public class WindowsButton : IButton
{
public void Render() => Console.WriteLine("Rendering a Windows button");
public void OnClick() => Console.WriteLine("Windows button clicked");
}

public class WindowsCheckbox : ICheckbox
{
public void Render() => Console.WriteLine("Rendering a Windows checkbox");
public void OnCheck() => Console.WriteLine("Windows checkbox checked");
}

// Concrete products for macOS
public class MacOSButton : IButton
{
public void Render() => Console.WriteLine("Rendering a macOS button");
public void OnClick() => Console.WriteLine("macOS button clicked");
}

public class MacOSCheckbox : ICheckbox
{
public void Render() => Console.WriteLine("Rendering a macOS checkbox");
public void OnCheck() => Console.WriteLine("macOS checkbox checked");
}

// Abstract factory
public interface IGUIFactory
{
IButton CreateButton();
ICheckbox CreateCheckbox();
}

// Concrete factories
public class WindowsFactory : IGUIFactory
{
public IButton CreateButton() => new WindowsButton();
public ICheckbox CreateCheckbox() => new WindowsCheckbox();
}

public class MacOSFactory : IGUIFactory
{
public IButton CreateButton() => new MacOSButton();
public ICheckbox CreateCheckbox() => new MacOSCheckbox();
}

// Client code
public class Application
{
private readonly IButton _button;
private readonly ICheckbox _checkbox;

public Application(IGUIFactory factory)
{
_button = factory.CreateButton();
_checkbox = factory.CreateCheckbox();
}

public void RenderUI()
{
_button.Render();
_checkbox.Render();
}

public void PerformOperations()
{
_button.OnClick();
_checkbox.OnCheck();
}
}

When to Use

  • When a system should be independent of how its products are created, composed, and represented
  • When a system should be configured with one of multiple families of products
  • When a family of related product objects is designed to be used together, and you need to enforce this constraint
  • When you want to provide a class library of products, and you want to reveal just their interfaces, not their implementations

9.2.4 - Builder Pattern

The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations.

Basic Implementation

// Product
public class Product
{
private List<string> _parts = new List<string>();

public void Add(string part)
{
_parts.Add(part);
}

public string ListParts()
{
return string.Join(", ", _parts);
}
}

// Builder interface
public interface IBuilder
{
void BuildPartA();
void BuildPartB();
void BuildPartC();
Product GetProduct();
}

// Concrete builder
public class ConcreteBuilder : IBuilder
{
private Product _product = new Product();

public ConcreteBuilder()
{
Reset();
}

public void Reset()
{
_product = new Product();
}

public void BuildPartA()
{
_product.Add("PartA");
}

public void BuildPartB()
{
_product.Add("PartB");
}

public void BuildPartC()
{
_product.Add("PartC");
}

public Product GetProduct()
{
Product result = _product;
Reset();
return result;
}
}

// Director
public class Director
{
private IBuilder _builder;

public Director(IBuilder builder)
{
_builder = builder;
}

public void ChangeBuilder(IBuilder builder)
{
_builder = builder;
}

public void BuildMinimalViableProduct()
{
_builder.BuildPartA();
}

public void BuildFullFeaturedProduct()
{
_builder.BuildPartA();
_builder.BuildPartB();
_builder.BuildPartC();
}
}

Fluent Builder Example

public class Email
{
public string From { get; set; }
public string To { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public bool IsHtml { get; set; }
public List<string> Attachments { get; set; } = new List<string>();
}

public class EmailBuilder
{
private readonly Email _email = new Email();

public EmailBuilder From(string from)
{
_email.From = from;
return this;
}

public EmailBuilder To(string to)
{
_email.To = to;
return this;
}

public EmailBuilder Subject(string subject)
{
_email.Subject = subject;
return this;
}

public EmailBuilder Body(string body, bool isHtml = false)
{
_email.Body = body;
_email.IsHtml = isHtml;
return this;
}

public EmailBuilder AddAttachment(string attachment)
{
_email.Attachments.Add(attachment);
return this;
}

public Email Build()
{
return _email;
}
}

// Usage
var email = new EmailBuilder()
.From("sender@example.com")
.To("recipient@example.com")
.Subject("Meeting Reminder")
.Body("<p>Don't forget our meeting tomorrow!</p>", true)
.AddAttachment("agenda.pdf")
.Build();

When to Use

  • When the algorithm for creating a complex object should be independent of the parts that make up the object and how they're assembled
  • When the construction process must allow different representations for the object that's constructed
  • When you need to construct complex objects step by step

9.2.5 - Prototype Pattern

The Prototype pattern specifies the kinds of objects to create using a prototypical instance, and creates new objects by copying this prototype.

Basic Implementation

// Prototype interface
public interface ICloneable<T>
{
T Clone();
}

// Concrete prototype
public class Person : ICloneable<Person>
{
public string Name { get; set; }
public int Age { get; set; }
public Address Address { get; set; }

public Person(string name, int age, Address address)
{
Name = name;
Age = age;
Address = address;
}

// Shallow copy
public Person ShallowClone()
{
return (Person)this.MemberwiseClone();
}

// Deep copy
public Person Clone()
{
var clone = (Person)this.MemberwiseClone();
clone.Address = Address.Clone();
return clone;
}
}

public class Address : ICloneable<Address>
{
public string Street { get; set; }
public string City { get; set; }
public string Country { get; set; }

public Address(string street, string city, string country)
{
Street = street;
City = city;
Country = country;
}

public Address Clone()
{
return new Address(Street, City, Country);
}
}

Using ICloneable Interface

public class Document : ICloneable
{
public string Title { get; set; }
public string Content { get; set; }
public List<string> Authors { get; set; }

public Document(string title, string content, List<string> authors)
{
Title = title;
Content = content;
Authors = authors;
}

public object Clone()
{
var clone = (Document)this.MemberwiseClone();
clone.Authors = new List<string>(Authors);
return clone;
}
}

When to Use

  • When the classes to instantiate are specified at run-time
  • When you need to avoid building a class hierarchy of factories that parallels the class hierarchy of products
  • When instances of a class can have one of only a few different combinations of state
  • When object creation is expensive compared to cloning

Considerations

  • The built-in ICloneable interface in .NET doesn't specify whether the clone should be deep or shallow
  • For complex objects, implementing a proper deep clone can be challenging
  • Consider using serialization for deep cloning of complex objects

9.2.6 - Understanding Creational Patterns: A Step-by-Step Guide for Beginners

Creational patterns can be challenging to understand at first, especially if you're new to design patterns. Let's break them down with simple explanations and practical advice on when to use each one.

When to Use Each Creational Pattern

  1. Singleton Pattern

    • Use when: You need exactly one instance of a class that should be accessible globally
    • Real-world examples: Database connection managers, logging services, configuration managers
    • Key benefit: Ensures a class has only one instance throughout the application
    • Potential drawback: Can make testing difficult and create hidden dependencies
  2. Factory Method Pattern

    • Use when: You don't know exactly which class you need to instantiate until runtime
    • Real-world examples: Document creators, UI element factories based on operating system
    • Key benefit: Decouples the client code from the concrete classes it creates
    • Potential drawback: Can lead to many small factory classes
  3. Abstract Factory Pattern

    • Use when: Your system needs to work with families of related objects without depending on their concrete classes
    • Real-world examples: UI toolkits that need to work across different operating systems
    • Key benefit: Ensures that created objects work together correctly
    • Potential drawback: Adding new types of products requires changing multiple interfaces and classes
  4. Builder Pattern

    • Use when: You need to create complex objects step by step with many optional components
    • Real-world examples: Creating complex documents, meal orders, or configuration objects
    • Key benefit: Separates construction from representation and controls the construction process
    • Potential drawback: Can lead to many builder classes for different representations
  5. Prototype Pattern

    • Use when: Creating new objects by copying existing ones is more efficient than creating from scratch
    • Real-world examples: Creating multiple similar documents or objects with slight variations
    • Key benefit: Hides the complexity of creating new instances
    • Potential drawback: Implementing deep copying can be complex for objects with many references

Common Mistakes to Avoid

  1. Overusing Singleton

    • Singletons are often overused. Before implementing a Singleton, consider if dependency injection would be a better approach.
  2. Creating Unnecessary Factories

    • Don't create factory classes when simple constructors would suffice. Use factories when you need the flexibility they provide.
  3. Ignoring Thread Safety

    • When implementing Singleton in multi-threaded environments, ensure thread safety using techniques like double-check locking or Lazy<T>.
  4. Confusing Deep vs. Shallow Copying

    • In the Prototype pattern, be clear about whether you're implementing deep or shallow copying, and document this for other developers.

Practical Implementation Tips

  1. For Singleton:

    • In modern C#, prefer the Lazy<T> implementation for thread safety and simplicity.
    • Consider making your Singleton class sealed to prevent inheritance, which could break the singleton pattern.
  2. For Factory Method:

    • Use abstract classes or interfaces to define the factory method.
    • Consider using generic factory methods when appropriate.
  3. For Abstract Factory:

    • Keep the interfaces focused and cohesive.
    • Consider using a factory of factories when dealing with very complex systems.
  4. For Builder:

    • Use the fluent interface pattern (method chaining) for a more readable API.
    • Consider implementing a director class to encapsulate common construction sequences.
  5. For Prototype:

    • For complex objects, consider using serialization for deep cloning.
    • Clearly document whether your clone methods perform deep or shallow copying.

Choosing Between Creational Patterns

Sometimes it can be difficult to choose which creational pattern to use. Here's a simple decision guide:

  • If you need exactly one instance: Singleton
  • If you need to create objects without specifying the exact class: Factory Method
  • If you need to create families of related objects: Abstract Factory
  • If you need to create complex objects step by step: Builder
  • If you need to create objects by copying existing ones: Prototype

Remember, design patterns are tools, not rules. Use them when they solve a problem, not just because they exist.