Design Patterns in C# with Examples: From A to Z
Design patterns are proven solutions to common problems that arise during software development. They are not ready-made code, but rather templates that can be adapted to a specific situation. For a C# developer, knowledge of patterns is not just a plus on a resume, but a necessity for creating flexible, maintainable, and scalable architecture.
In this article, we will break down the three main groups of patterns according to the GoF (Gang of Four) classification: creational, structural, and behavioral. Each pattern will be examined with a practical example in C# (.NET).
Creational Patterns
These patterns manage the process of object creation, making the system independent of how objects are created. Let's look at the two most popular ones.
Singleton
Ensures that a class has only one instance and provides a global point of access to it. In C#, Lazy<T> is used for safe operation in a multi-threaded environment.
public sealed class Logger{ private static readonly Lazy<Logger> _instance = new Lazy<Logger>(() => new Logger());
public static Logger Instance => _instance.Value;
private Logger() { }
public void Log(string message) { Console.WriteLine($"[{DateTime.Now}] {message}"); }}
// UsageLogger.Instance.Log("Application started");Factory Method
Defines an interface for creating an object, but lets subclasses alter the type of object that will be created. It is well-suited for situations where the type of object is not known in advance.
public interface ITransport{ void Deliver();}
public class Truck : ITransport{ public void Deliver() => Console.WriteLine("Delivery by land");}
public class Ship : ITransport{ public void Deliver() => Console.WriteLine("Delivery by sea");}
public abstract class Logistics{ public abstract ITransport CreateTransport();
public void PlanDelivery() { var transport = CreateTransport(); transport.Deliver(); }}
public class RoadLogistics : Logistics{ public override ITransport CreateTransport() => new Truck();}
// UsageLogistics logistics = new RoadLogistics();logistics.PlanDelivery(); // Output: Delivery by landStructural Patterns
These patterns define ways to compose relationships between objects, ensuring flexibility and efficiency of the architecture.
Adapter
Allows objects with incompatible interfaces to work together. It is often used when integrating with legacy code or third-party libraries.
// Target interface expected by the clientpublic interface IModernPrinter{ void Print(string text);}
// Old class with an incompatible interfacepublic class OldPrinter{ public void PrintOldStyle(string message) { Console.WriteLine($"Old printer: {message}"); }}
// Adapterpublic class PrinterAdapter : IModernPrinter{ private readonly OldPrinter _oldPrinter;
public PrinterAdapter(OldPrinter oldPrinter) { _oldPrinter = oldPrinter; }
public void Print(string text) { _oldPrinter.PrintOldStyle(text); }}
// UsageIModernPrinter printer = new PrinterAdapter(new OldPrinter());printer.Print("Hello, world!"); // Output: Old printer: Hello, world!Decorator
Dynamically adds new responsibilities to an object without modifying its code. It is a flexible alternative to inheritance.
public interface ICoffee{ double GetCost(); string GetDescription();}
public class SimpleCoffee : ICoffee{