C# Design Patterns Implementation Guide

Design patterns are reusable solutions to commonly occurring problems in software design. They represent best practices and provide a template for how to solve problems that can be used in many differ

Introduction to Design Patterns#

Design patterns are reusable solutions to commonly occurring problems in software design. They represent best practices and provide a template for how to solve problems that can be used in many different situations. This guide covers the most important design patterns in C# with practical examples and real-world applications.

Design patterns are typically categorized into three main types: Creational, Structural, and Behavioral patterns.

Creational Patterns#

Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.

1. Singleton Pattern#

Ensures a class has only one instance and provides a global point of access to it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Thread-safe Singleton using Lazy<T>
public sealed class Logger
{
    private static readonly Lazy<Logger> _instance = 
        new Lazy<Logger>(() => new Logger());
    
    private Logger()
    {
        // Private constructor prevents external instantiation
    }
    
    public static Logger Instance => _instance.Value;
    
    public void Log(string message)
    {
        Console.WriteLine($"[{DateTime.Now}] {message}");
    }
}

// Usage
Logger.Instance.Log("Application started");

Alternative: Thread-safe with double-check locking:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public sealed class DatabaseConnection
{
    private static DatabaseConnection _instance;
    private static readonly object _lock = new object();
    
    private DatabaseConnection()
    {
        // Initialize connection
    }
    
    public static DatabaseConnection Instance
    {
        get
        {
            if (_instance == null)
            {
                lock (_lock)
                {
                    if (_instance == null)
                    {
                        _instance = new DatabaseConnection();
                    }
                }
            }
            return _instance;
        }
    }
}

When to use:

  • Need exactly one instance of a class
  • Global access point required
  • Examples: Logger, Configuration, Cache

Caution: Singletons can make testing difficult and introduce global state.

2. Factory Pattern#

Creates objects without specifying the exact class to create.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Product interface
public interface IPaymentProcessor
{
    PaymentResult Process(Payment payment);
}

// Concrete products
public class CreditCardProcessor : IPaymentProcessor
{
    public PaymentResult Process(Payment payment)
    {
        Console.WriteLine("Processing credit card payment");
        return new PaymentResult { Success = true };
    }
}

public class PayPalProcessor : IPaymentProcessor
{
    public PaymentResult Process(Payment payment)
    {
        Console.WriteLine("Processing PayPal payment");
        return new PaymentResult { Success = true };
    }
}

public class CryptoProcessor : IPaymentProcessor
{
    public PaymentResult Process(Payment payment)
    {
        Console.WriteLine("Processing cryptocurrency payment");
        return new PaymentResult { Success = true };
    }
}

// Factory
public class PaymentProcessorFactory
{
    public IPaymentProcessor CreateProcessor(PaymentMethod method)
    {
        return method switch
        {
            PaymentMethod.CreditCard => new CreditCardProcessor(),
            PaymentMethod.PayPal => new PayPalProcessor(),
            PaymentMethod.Crypto => new CryptoProcessor(),
            _ => throw new ArgumentException("Invalid payment method")
        };
    }
}

// Usage
var factory = new PaymentProcessorFactory();
var processor = factory.CreateProcessor(PaymentMethod.CreditCard);
var result = processor.Process(payment);

When to use:

  • Object creation is complex
  • Need to decouple object creation from usage
  • Create different types based on conditions

3. Abstract Factory Pattern#

Provides an interface for creating families of related objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// Abstract products
public interface IButton
{
    void Render();
}

public interface ICheckbox
{
    void Render();
}

// Concrete products - 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 - 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 WindowsFactory : IUIFactory
{
    public IButton CreateButton() => new WindowsButton();
    public ICheckbox CreateCheckbox() => new WindowsCheckbox();
}

public class MacFactory : IUIFactory
{
    public IButton CreateButton() => new MacButton();
    public ICheckbox CreateCheckbox() => new MacCheckbox();
}

// Usage
public class Application
{
    private readonly IUIFactory _factory;
    private IButton _button;
    private ICheckbox _checkbox;
    
    public Application(IUIFactory factory)
    {
        _factory = factory;
    }
    
    public void CreateUI()
    {
        _button = _factory.CreateButton();
        _checkbox = _factory.CreateCheckbox();
    }
    
    public void Render()
    {
        _button.Render();
        _checkbox.Render();
    }
}

// Client code
IUIFactory factory = OperatingSystem.IsWindows() 
    ? new WindowsFactory() 
    : new MacFactory();

var app = new Application(factory);
app.CreateUI();
app.Render();

When to use:

  • Create families of related objects
  • Ensure compatibility between created objects
  • Examples: UI themes, database providers

4. Builder Pattern#

Constructs complex objects step by step.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class HttpRequest
{
    public string Method { get; set; }
    public string Url { get; set; }
    public Dictionary<string, string> Headers { get; set; }
    public string Body { get; set; }
    public TimeSpan Timeout { get; set; }
}

public class HttpRequestBuilder
{
    private readonly HttpRequest _request;
    
    public HttpRequestBuilder()
    {
        _request = new HttpRequest
        {
            Headers = new Dictionary<string, string>(),
            Timeout = TimeSpan.FromSeconds(30)
        };
    }
    
    public HttpRequestBuilder WithMethod(string method)
    {
        _request.Method = method;
        return this;
    }
    
    public HttpRequestBuilder WithUrl(string url)
    {
        _request.Url = url;
        return this;
    }
    
    public HttpRequestBuilder AddHeader(string key, string value)
    {
        _request.Headers[key] = value;
        return this;
    }
    
    public HttpRequestBuilder WithBody(string body)
    {
        _request.Body = body;
        return this;
    }
    
    public HttpRequestBuilder WithTimeout(TimeSpan timeout)
    {
        _request.Timeout = timeout;
        return this;
    }
    
    public HttpRequest Build()
    {
        if (string.IsNullOrEmpty(_request.Method))
            throw new InvalidOperationException("Method is required");
        if (string.IsNullOrEmpty(_request.Url))
            throw new InvalidOperationException("URL is required");
        
        return _request;
    }
}

// Usage - Fluent interface
var request = new HttpRequestBuilder()
    .WithMethod("POST")
    .WithUrl("https://api.example.com/users")
    .AddHeader("Content-Type", "application/json")
    .AddHeader("Authorization", "Bearer token123")
    .WithBody("{\"name\":\"John\"}")
    .WithTimeout(TimeSpan.FromSeconds(60))
    .Build();

When to use:

  • Complex object construction
  • Many optional parameters
  • Step-by-step construction required

5. Prototype Pattern#

Creates new objects by copying existing ones.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public interface ICloneable<T>
{
    T Clone();
}

public class Document : ICloneable<Document>
{
    public string Title { get; set; }
    public string Content { get; set; }
    public List<string> Tags { get; set; }
    public DateTime CreatedDate { get; set; }
    
    public Document Clone()
    {
        return new Document
        {
            Title = this.Title,
            Content = this.Content,
            Tags = new List<string>(this.Tags),
            CreatedDate = DateTime.Now
        };
    }
}

// Usage
var original = new Document
{
    Title = "Original",
    Content = "Content",
    Tags = new List<string> { "tag1", "tag2" },
    CreatedDate = DateTime.Now
};

var copy = original.Clone();
copy.Title = "Copy";
copy.Tags.Add("tag3");

// Original unchanged
Console.WriteLine(original.Tags.Count); // 2
Console.WriteLine(copy.Tags.Count);     // 3

When to use:

  • Object creation is expensive
  • Need many similar objects
  • Want to avoid subclassing

Structural Patterns#

Structural patterns explain how to assemble objects and classes into larger structures.

1. Adapter Pattern#

Converts interface of a class into another interface clients expect.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Target interface expected by client
public interface IEmailSender
{
    Task SendEmailAsync(string to, string subject, string body);
}

// Adaptee - third-party library with different interface
public class ThirdPartyEmailService
{
    public void Send(EmailMessage message)
    {
        Console.WriteLine($"Sending via third-party: {message.Recipient}");
    }
}

public class EmailMessage
{
    public string Recipient { get; set; }
    public string Subject { get; set; }
    public string Body { get; set; }
}

// Adapter
public class ThirdPartyEmailAdapter : IEmailSender
{
    private readonly ThirdPartyEmailService _service;
    
    public ThirdPartyEmailAdapter(ThirdPartyEmailService service)
    {
        _service = service;
    }
    
    public Task SendEmailAsync(string to, string subject, string body)
    {
        var message = new EmailMessage
        {
            Recipient = to,
            Subject = subject,
            Body = body
        };
        
        _service.Send(message);
        return Task.CompletedTask;
    }
}

// Usage
IEmailSender emailSender = new ThirdPartyEmailAdapter(
    new ThirdPartyEmailService()
);

await emailSender.SendEmailAsync(
    "user@example.com",
    "Hello",
    "Message body"
);

When to use:

  • Use existing class with incompatible interface
  • Integrate third-party libraries
  • Make incompatible interfaces work together

2. Decorator Pattern#

Adds new functionality to objects dynamically.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// Component interface
public interface IDataSource
{
    void WriteData(string data);
    string ReadData();
}

// Concrete component
public class FileDataSource : IDataSource
{
    private readonly string _filename;
    
    public FileDataSource(string filename)
    {
        _filename = filename;
    }
    
    public void WriteData(string data)
    {
        File.WriteAllText(_filename, data);
    }
    
    public string ReadData()
    {
        return File.ReadAllText(_filename);
    }
}

// Base decorator
public abstract class DataSourceDecorator : IDataSource
{
    protected readonly IDataSource _wrappee;
    
    public DataSourceDecorator(IDataSource source)
    {
        _wrappee = source;
    }
    
    public virtual void WriteData(string data)
    {
        _wrappee.WriteData(data);
    }
    
    public virtual string ReadData()
    {
        return _wrappee.ReadData();
    }
}

// Concrete decorator - Encryption
public class EncryptionDecorator : DataSourceDecorator
{
    public EncryptionDecorator(IDataSource source) : base(source) { }
    
    public override void WriteData(string data)
    {
        var encrypted = Encrypt(data);
        base.WriteData(encrypted);
    }
    
    public override string ReadData()
    {
        var encrypted = base.ReadData();
        return Decrypt(encrypted);
    }
    
    private string Encrypt(string data)
    {
        // Simple encryption for demo
        return Convert.ToBase64String(Encoding.UTF8.GetBytes(data));
    }
    
    private string Decrypt(string data)
    {
        return Encoding.UTF8.GetString(Convert.FromBase64String(data));
    }
}

// Concrete decorator - Compression
public class CompressionDecorator : DataSourceDecorator
{
    public CompressionDecorator(IDataSource source) : base(source) { }
    
    public override void WriteData(string data)
    {
        var compressed = Compress(data);
        base.WriteData(compressed);
    }
    
    public override string ReadData()
    {
        var compressed = base.ReadData();
        return Decompress(compressed);
    }
    
    private string Compress(string data)
    {
        // Compression logic
        return data; // Simplified
    }
    
    private string Decompress(string data)
    {
        // Decompression logic
        return data; // Simplified
    }
}

// Usage - Stack decorators
IDataSource source = new FileDataSource("data.txt");
source = new EncryptionDecorator(source);
source = new CompressionDecorator(source);

source.WriteData("Sensitive data");
string data = source.ReadData(); // Decompressed and decrypted

When to use:

  • Add responsibilities to objects dynamically
  • Extend functionality without subclassing
  • Compose behaviors

3. Facade Pattern#

Provides a simplified interface to a complex subsystem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Complex subsystem
public class VideoFile
{
    public string Filename { get; set; }
    public string Codec { get; set; }
}

public class CodecFactory
{
    public ICodec Extract(VideoFile file)
    {
        return file.Codec switch
        {
            "mp4" => new MPEG4Codec(),
            "ogg" => new OggCodec(),
            _ => throw new Exception("Unsupported codec")
        };
    }
}

public interface ICodec { }
public class MPEG4Codec : ICodec { }
public class OggCodec : ICodec { }

public class BitrateReader
{
    public VideoFile Read(string filename)
    {
        return new VideoFile { Filename = filename, Codec = "mp4" };
    }
    
    public VideoFile Convert(VideoFile file, ICodec codec)
    {
        return file;
    }
}

public class AudioMixer
{
    public void Mix(VideoFile file)
    {
        Console.WriteLine("Mixing audio");
    }
}

// Facade - simplifies the complex subsystem
public class VideoConverter
{
    public void Convert(string filename, string format)
    {
        var file = new BitrateReader().Read(filename);
        var codec = new CodecFactory().Extract(file);
        var converted = new BitrateReader().Convert(file, codec);
        new AudioMixer().Mix(converted);
        
        Console.WriteLine($"Converted {filename} to {format}");
    }
}

// Usage - Simple interface
var converter = new VideoConverter();
converter.Convert("video.ogg", "mp4");

When to use:

  • Simplify complex systems
  • Provide a single entry point
  • Decouple subsystems from clients

4. Proxy Pattern#

Provides a placeholder for another object to control access.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// Subject interface
public interface IImage
{
    void Display();
}

// Real subject
public class RealImage : IImage
{
    private readonly string _filename;
    
    public RealImage(string filename)
    {
        _filename = filename;
        LoadFromDisk();
    }
    
    private void LoadFromDisk()
    {
        Console.WriteLine($"Loading image from disk: {_filename}");
        Thread.Sleep(1000); // Simulate expensive operation
    }
    
    public void Display()
    {
        Console.WriteLine($"Displaying {_filename}");
    }
}

// Proxy - lazy loading
public class ProxyImage : IImage
{
    private readonly string _filename;
    private RealImage _realImage;
    
    public ProxyImage(string filename)
    {
        _filename = filename;
    }
    
    public void Display()
    {
        // Lazy loading - only create real image when needed
        if (_realImage == null)
        {
            _realImage = new RealImage(_filename);
        }
        _realImage.Display();
    }
}

// Usage
IImage image = new ProxyImage("photo.jpg");
// Image not loaded yet

image.Display(); // Loads and displays
image.Display(); // Just displays (already loaded)

Types of Proxies:

  • Virtual Proxy: Lazy initialization
  • Protection Proxy: Access control
  • Remote Proxy: Represents remote object
  • Caching Proxy: Caches results

When to use:

  • Control access to objects
  • Lazy initialization
  • Access control
  • Logging, caching

Behavioral Patterns#

Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects.

1. Strategy Pattern#

Defines a family of algorithms and makes them interchangeable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// Strategy interface
public interface ISortStrategy
{
    void Sort(List<int> list);
}

// Concrete strategies
public class QuickSort : ISortStrategy
{
    public void Sort(List<int> list)
    {
        Console.WriteLine("Sorting using QuickSort");
        list.Sort(); // Simplified
    }
}

public class MergeSort : ISortStrategy
{
    public void Sort(List<int> list)
    {
        Console.WriteLine("Sorting using MergeSort");
        list.Sort(); // Simplified
    }
}

public class BubbleSort : ISortStrategy
{
    public void Sort(List<int> list)
    {
        Console.WriteLine("Sorting using BubbleSort");
        for (int i = 0; i < list.Count - 1; i++)
        {
            for (int j = 0; j < list.Count - i - 1; j++)
            {
                if (list[j] > list[j + 1])
                {
                    (list[j], list[j + 1]) = (list[j + 1], list[j]);
                }
            }
        }
    }
}

// Context
public class SortContext
{
    private ISortStrategy _strategy;
    
    public void SetStrategy(ISortStrategy strategy)
    {
        _strategy = strategy;
    }
    
    public void Sort(List<int> list)
    {
        _strategy.Sort(list);
    }
}

// Usage
var numbers = new List<int> { 5, 2, 8, 1, 9 };
var context = new SortContext();

// Choose strategy based on data size
if (numbers.Count < 10)
{
    context.SetStrategy(new BubbleSort());
}
else if (numbers.Count < 1000)
{
    context.SetStrategy(new QuickSort());
}
else
{
    context.SetStrategy(new MergeSort());
}

context.Sort(numbers);

When to use:

  • Multiple algorithms for same task
  • Switch algorithms at runtime
  • Avoid conditional statements

2. Observer Pattern#

Defines one-to-many dependency between objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// Subject
public class Stock
{
    private readonly List<IInvestor> _investors = new();
    private string _symbol;
    private double _price;
    
    public Stock(string symbol, double price)
    {
        _symbol = symbol;
        _price = price;
    }
    
    public void Attach(IInvestor investor)
    {
        _investors.Add(investor);
    }
    
    public void Detach(IInvestor investor)
    {
        _investors.Remove(investor);
    }
    
    public void Notify()
    {
        foreach (var investor in _investors)
        {
            investor.Update(this);
        }
    }
    
    public double Price
    {
        get => _price;
        set
        {
            if (_price != value)
            {
                _price = value;
                Notify();
            }
        }
    }
    
    public string Symbol => _symbol;
}

// Observer interface
public interface IInvestor
{
    void Update(Stock stock);
}

// Concrete observers
public class Investor : IInvestor
{
    private readonly string _name;
    
    public Investor(string name)
    {
        _name = name;
    }
    
    public void Update(Stock stock)
    {
        Console.WriteLine(
            $"Notified {_name} of {stock.Symbol}'s change to {stock.Price:C}"
        );
    }
}

// Usage
var stock = new Stock("MSFT", 120.00);

var investor1 = new Investor("John");
var investor2 = new Investor("Jane");

stock.Attach(investor1);
stock.Attach(investor2);

stock.Price = 125.50; // Both investors notified
stock.Price = 130.00; // Both investors notified

stock.Detach(investor1);
stock.Price = 135.00; // Only Jane notified

Modern C# - Using events:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Stock
{
    public event EventHandler<PriceChangedEventArgs> PriceChanged;
    
    private double _price;
    public double Price
    {
        get => _price;
        set
        {
            if (_price != value)
            {
                _price = value;
                OnPriceChanged(new PriceChangedEventArgs(_price));
            }
        }
    }
    
    protected virtual void OnPriceChanged(PriceChangedEventArgs e)
    {
        PriceChanged?.Invoke(this, e);
    }
}

public class PriceChangedEventArgs : EventArgs
{
    public double NewPrice { get; }
    
    public PriceChangedEventArgs(double newPrice)
    {
        NewPrice = newPrice;
    }
}

// Usage
var stock = new Stock();
stock.PriceChanged += (sender, e) =>
{
    Console.WriteLine($"Price changed to {e.NewPrice}");
};

stock.Price = 125.50;

When to use:

  • One object changes, others need to react
  • Publish-subscribe scenarios
  • Event-driven systems

3. Command Pattern#

Encapsulates a request as an object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// Command interface
public interface ICommand
{
    void Execute();
    void Undo();
}

// Receiver
public class Light
{
    public void TurnOn()
    {
        Console.WriteLine("Light is ON");
    }
    
    public void TurnOff()
    {
        Console.WriteLine("Light is OFF");
    }
}

// Concrete commands
public class LightOnCommand : ICommand
{
    private readonly Light _light;
    
    public LightOnCommand(Light light)
    {
        _light = light;
    }
    
    public void Execute()
    {
        _light.TurnOn();
    }
    
    public void Undo()
    {
        _light.TurnOff();
    }
}

public class LightOffCommand : ICommand
{
    private readonly Light _light;
    
    public LightOffCommand(Light light)
    {
        _light = light;
    }
    
    public void Execute()
    {
        _light.TurnOff();
    }
    
    public void Undo()
    {
        _light.TurnOn();
    }
}

// Invoker
public class RemoteControl
{
    private readonly Stack<ICommand> _commandHistory = new();
    
    public void Submit(ICommand command)
    {
        command.Execute();
        _commandHistory.Push(command);
    }
    
    public void Undo()
    {
        if (_commandHistory.Count > 0)
        {
            var command = _commandHistory.Pop();
            command.Undo();
        }
    }
}

// Usage
var light = new Light();
var remote = new RemoteControl();

remote.Submit(new LightOnCommand(light));  // Light ON
remote.Submit(new LightOffCommand(light)); // Light OFF
remote.Undo();                              // Light ON (undo)

When to use:

  • Parameterize objects with operations
  • Queue operations
  • Support undo/redo
  • Log changes

Best Practices#

1. Don’t Force Patterns#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// BAD - Unnecessary pattern usage
public class SimpleCalculator
{
    // Singleton for a simple calculator? Overkill!
    private static readonly Lazy<SimpleCalculator> _instance = 
        new Lazy<SimpleCalculator>(() => new SimpleCalculator());
    
    public static SimpleCalculator Instance => _instance.Value;
}

// GOOD - Simple when simple suffices
public class SimpleCalculator
{
    public int Add(int a, int b) => a + b;
}

2. Combine Patterns Appropriately#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Factory + Strategy
public class PaymentProcessorFactory
{
    public IPaymentStrategy CreateStrategy(PaymentMethod method)
    {
        return method switch
        {
            PaymentMethod.CreditCard => new CreditCardStrategy(),
            PaymentMethod.PayPal => new PayPalStrategy(),
            _ => throw new ArgumentException("Invalid method")
        };
    }
}

// Decorator + Strategy
public interface ICompressionStrategy
{
    byte[] Compress(byte[] data);
}

public class CompressionDecorator : DataSourceDecorator
{
    private readonly ICompressionStrategy _strategy;
    
    public CompressionDecorator(
        IDataSource source, 
        ICompressionStrategy strategy) 
        : base(source)
    {
        _strategy = strategy;
    }
}

3. Consider Modern C# Features#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Pattern matching with strategy pattern
public decimal CalculateDiscount(Customer customer, decimal amount)
{
    return customer.Type switch
    {
        CustomerType.Regular => amount * 0.05m,
        CustomerType.Gold => amount * 0.10m,
        CustomerType.Platinum => amount * 0.15m,
        _ => 0m
    };
}

// Records for immutable objects
public record Product(int Id, string Name, decimal Price);

// Using statement for dispose pattern
public async Task ProcessFile(string path)
{
    using var stream = File.OpenRead(path);
    using var reader = new StreamReader(stream);
    var content = await reader.ReadToEndAsync();
}

Conclusion#

Design patterns are essential tools in a C# developer’s toolkit. Key takeaways:

  • Use patterns to solve recurring problems
  • Don’t force patterns where they don’t fit
  • Understand the intent behind each pattern
  • Combine patterns when appropriate
  • Leverage C# language features
  • Keep code simple and maintainable

By mastering these design patterns, you can write more maintainable, flexible, and testable code that follows industry best practices.

References#

Contents