Skip to main content

C# Design Patterns

Introduction

Design patterns are proven solutions to common problems that arise during software development. They represent best practices evolved over time by experienced software developers. In C#, an object-oriented language, design patterns provide templates for writing code that is maintainable, reusable, and extensible.

This guide will introduce you to the concept of design patterns in C#, explain their benefits, and walk through several important patterns with practical examples. By the end of this article, you'll have a solid foundation in implementing design patterns in your C# projects.

What Are Design Patterns?

Design patterns are standardized approaches to solving specific design problems in software development. They are not finished designs that can be directly transformed into code but rather templates that can be applied to different situations.

Design patterns are typically categorized into three groups:

  1. Creational Patterns: Focus on object creation mechanisms
  2. Structural Patterns: Deal with object composition and relationships
  3. Behavioral Patterns: Concerned with object interaction and responsibility

Why Use Design Patterns?

Before diving into specific patterns, let's understand why design patterns are valuable:

  • Proven Solutions: They represent industry-tested solutions to common problems
  • Shared Vocabulary: Makes it easier to communicate design ideas with team members
  • Code Reusability: Encourages writing reusable components
  • Scalability: Makes it easier to scale applications by following established patterns
  • Maintainability: Results in more maintainable and cleaner code

Common Design Patterns in C#

Let's explore some of the most widely used design patterns in C# development:

1. Singleton Pattern (Creational)

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.

Example Implementation:

csharp
public sealed class Singleton
{
private static Singleton _instance;
private static readonly object _lock = new object();

// Private constructor prevents instantiation from other classes
private Singleton() { }

public static Singleton Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
}

public void SomeBusinessLogic()
{
Console.WriteLine("Singleton method executed!");
}
}

Usage:

csharp
// Client code
public static void Main()
{
// Get the singleton instance
Singleton instance1 = Singleton.Instance;
instance1.SomeBusinessLogic();

// Try to get another instance - will be the same object
Singleton instance2 = Singleton.Instance;

// Check if both instances are the same
Console.WriteLine($"Are both instances the same? {ReferenceEquals(instance1, instance2)}");
}

Output:

Singleton method executed!
Are both instances the same? True

Real-world Applications:

  • Database connection pools
  • Application configuration
  • Logging frameworks
  • Cache implementations

2. Factory Method Pattern (Creational)

The Factory Method pattern defines an interface for creating objects but allows subclasses to alter the type of objects that will be created.

Example Implementation:

csharp
// Product interface
public interface IVehicle
{
void Drive();
}

// Concrete Products
public class Car : IVehicle
{
public void Drive()
{
Console.WriteLine("Driving a car...");
}
}

public class Motorcycle : IVehicle
{
public void Drive()
{
Console.WriteLine("Riding a motorcycle...");
}
}

// Creator abstract class
public abstract class VehicleFactory
{
public abstract IVehicle CreateVehicle();

public void DeliverVehicle()
{
IVehicle vehicle = CreateVehicle();

Console.WriteLine("Vehicle created and ready for delivery!");
vehicle.Drive();
}
}

// Concrete Creators
public class CarFactory : VehicleFactory
{
public override IVehicle CreateVehicle()
{
return new Car();
}
}

public class MotorcycleFactory : VehicleFactory
{
public override IVehicle CreateVehicle()
{
return new Motorcycle();
}
}

Usage:

csharp
public static void Main()
{
Console.WriteLine("Creating a car:");
VehicleFactory carFactory = new CarFactory();
carFactory.DeliverVehicle();

Console.WriteLine("\nCreating a motorcycle:");
VehicleFactory motorcycleFactory = new MotorcycleFactory();
motorcycleFactory.DeliverVehicle();
}

Output:

Creating a car:
Vehicle created and ready for delivery!
Driving a car...

Creating a motorcycle:
Vehicle created and ready for delivery!
Riding a motorcycle...

Real-world Applications:

  • UI element creation in frameworks
  • Database connector creation
  • Document generators (PDF, Word, etc.)
  • Plugin systems

3. Observer Pattern (Behavioral)

The Observer pattern defines a one-to-many dependency between objects so that when one object (the subject) changes state, all its dependents (observers) are notified and updated automatically.

Example Implementation:

csharp
// Observer interface
public interface IObserver
{
void Update(string message);
}

// Subject class
public class WeatherStation
{
private readonly List<IObserver> _observers = new List<IObserver>();
private string _weatherUpdate;

public void RegisterObserver(IObserver observer)
{
_observers.Add(observer);
}

public void RemoveObserver(IObserver observer)
{
_observers.Remove(observer);
}

public void NotifyObservers()
{
foreach (var observer in _observers)
{
observer.Update(_weatherUpdate);
}
}

public void SetWeatherUpdate(string weatherUpdate)
{
_weatherUpdate = weatherUpdate;
NotifyObservers();
}
}

// Concrete observer classes
public class NewsAgency : IObserver
{
private readonly string _name;

public NewsAgency(string name)
{
_name = name;
}

public void Update(string message)
{
Console.WriteLine($"{_name} received weather update: {message}");
}
}

Usage:

csharp
public static void Main()
{
// Create the subject
WeatherStation weatherStation = new WeatherStation();

// Create observers
IObserver bbcNews = new NewsAgency("BBC News");
IObserver cnnNews = new NewsAgency("CNN News");

// Register observers
weatherStation.RegisterObserver(bbcNews);
weatherStation.RegisterObserver(cnnNews);

// Update weather
Console.WriteLine("Weather station is sending first update:");
weatherStation.SetWeatherUpdate("Sunny day with temperature of 25°C");

// Remove one observer
weatherStation.RemoveObserver(cnnNews);

// Update weather again
Console.WriteLine("\nWeather station is sending second update:");
weatherStation.SetWeatherUpdate("Cloudy with a chance of rain");
}

Output:

Weather station is sending first update:
BBC News received weather update: Sunny day with temperature of 25°C
CNN News received weather update: Sunny day with temperature of 25°C

Weather station is sending second update:
BBC News received weather update: Cloudy with a chance of rain

Real-world Applications:

  • Event handling systems
  • Notification services
  • Message queues
  • User interface updates

4. Strategy Pattern (Behavioral)

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.

Example Implementation:

csharp
// Strategy Interface
public interface IPaymentStrategy
{
void Pay(int amount);
}

// Concrete Strategies
public class CreditCardStrategy : IPaymentStrategy
{
private readonly string _name;
private readonly string _cardNumber;
private readonly string _cvv;
private readonly string _expiryDate;

public CreditCardStrategy(string name, string cardNumber, string cvv, string expiryDate)
{
_name = name;
_cardNumber = cardNumber;
_cvv = cvv;
_expiryDate = expiryDate;
}

public void Pay(int amount)
{
Console.WriteLine($"${amount} paid with credit card (Card: {_cardNumber})");
}
}

public class PayPalStrategy : IPaymentStrategy
{
private readonly string _emailId;
private readonly string _password;

public PayPalStrategy(string emailId, string password)
{
_emailId = emailId;
_password = password;
}

public void Pay(int amount)
{
Console.WriteLine($"${amount} paid using PayPal (Account: {_emailId})");
}
}

// Context class
public class ShoppingCart
{
private readonly List<int> _prices = new List<int>();

public void AddItem(int price)
{
_prices.Add(price);
}

public void Pay(IPaymentStrategy paymentMethod)
{
int total = _prices.Sum();
paymentMethod.Pay(total);
}
}

Usage:

csharp
public static void Main()
{
// Create shopping cart and add items
ShoppingCart cart = new ShoppingCart();
cart.AddItem(100);
cart.AddItem(50);
cart.AddItem(75);

// Pay with different payment strategies
Console.WriteLine("Customer 1 pays with credit card:");
cart.Pay(new CreditCardStrategy("John Doe", "1234567890123456", "123", "12/25"));

Console.WriteLine("\nCustomer 2 pays with PayPal:");
cart.Pay(new PayPalStrategy("[email protected]", "password"));
}

Output:

Customer 1 pays with credit card:
$225 paid with credit card (Card: 1234567890123456)

Customer 2 pays with PayPal:
$225 paid using PayPal (Account: [email protected])

Real-world Applications:

  • Payment processing systems
  • Sorting algorithms
  • Validation strategies
  • File compression algorithms

Implementing Design Patterns: Best Practices

When implementing design patterns in your C# projects, keep these best practices in mind:

  1. Don't Force Patterns: Use patterns only when they solve a specific problem you have.
  2. KISS (Keep It Simple, Stupid): Sometimes a simple solution is better than a complex pattern.
  3. Combine Patterns: Multiple patterns often work together to solve complex design problems.
  4. Know the Trade-offs: Understand the advantages and disadvantages of each pattern.
  5. Use Interfaces: Most patterns rely heavily on interfaces for flexibility.
  6. Document Your Implementation: Explain why a pattern was used in comments or documentation.

Summary

Design patterns are powerful tools in a C# developer's toolkit. They provide standardized solutions to common software design problems, making code more maintainable, reusable, and extensible. In this guide, we've covered:

  • What design patterns are and why they're important
  • The three main categories of design patterns
  • Detailed implementations of four common patterns:
    • Singleton (Creational)
    • Factory Method (Creational)
    • Observer (Behavioral)
    • Strategy (Behavioral)
  • Best practices for implementing design patterns in your C# projects

By understanding and applying these design patterns, you'll be well on your way to writing cleaner, more efficient, and more maintainable C# code.

Additional Resources and Exercises

Resources

Exercises

  1. Implement the Decorator Pattern: Create a coffee ordering system where different toppings can be added to a base coffee.

  2. Build a Command Pattern Example: Implement a remote control that can issue various commands to home appliances.

  3. Create an Adapter Pattern: Build an adapter that allows a new library to work with an existing codebase that expects a different interface.

  4. Implement the Builder Pattern: Create a complex object construction system, such as a meal builder for a restaurant ordering system.

  5. Design a Chain of Responsibility: Implement an approval workflow where requests pass through multiple handlers until they are either processed or rejected.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)