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.