Post

C# Design Patterns Implementation Guide

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

This post is licensed under CC BY 4.0 by the author.