Design patterns are like recipes for solving common problems in software design. They're not code you copy-paste, but smart ideas you can apply to real-world problems. They help make your code more flexible, reusable, and easier to understand.
There are three main categories of design patterns:
- Creational: How objects are created.
- Structural: How objects are connected.
- Behavioral: How objects interact.
🧱 Creational Patterns
1. Singleton Pattern
Use when: You want only one instance of a class (like a single Logger or Configuration).
public sealed class Logger {
private static readonly Logger _instance = new Logger();
private Logger() {}
public static Logger Instance => _instance;
public void Log(string msg) => Console.WriteLine(msg);
}
Simple Explanation:
Imagine you want just one diary that everyone writes to. The Singleton ensures there's only one diary for the whole app.
2. Factory Pattern
Use when: You want to decide which object to create at runtime based on input.
public abstract class Notification { public abstract void Send(string to);}public class EmailNotification : Notification { public override void Send(string to) => Console.WriteLine($"Email sent to {to}");}public class SMSNotification : Notification { public override void Send(string to) => Console.WriteLine($"SMS sent to {to}");}public class NotificationFactory { public static Notification Create(string type) { return type switch { "Email" => new EmailNotification(), "SMS" => new SMSNotification(), _ => throw new ArgumentException("Invalid type") }; }}Simple Explanation:
It’s like a pizza shop. You order by saying "Veg" or "Cheese" and get the correct pizza. You don't care how it's made.
3. Builder Pattern
Use when: You want to build complex objects step-by-step.
public class Car { public string Engine { get; set; } public string Wheels { get; set; } public string Color { get; set; } } public class CarBuilder { private Car car = new Car(); public CarBuilder SetEngine(string engine) { car.Engine = engine; return this; } public CarBuilder SetWheels(string wheels) { car.Wheels = wheels; return this; } public CarBuilder SetColor(string color) { car.Color = color; return this; } public Car Build() => car; }Simple Explanation:
Like building a burger: first the bun, then the patty, then sauce. You control the steps.
🪩 Structural Patterns
1. Adapter Pattern
Use when: You have incompatible classes that need to work together.
public interface ITarget { void Request(); } public class Adaptee { public void SpecificRequest() => Console.WriteLine("Specific Request Called"); } public class Adapter : ITarget { private Adaptee _adaptee = new Adaptee(); public void Request() => _adaptee.SpecificRequest(); }Simple Explanation:
It’s like using a travel adapter for your charger in a different country.
2. Decorator Pattern
Use when: You want to add features to an object without changing its code.
public interface IMessage { string GetMessage(); } public class SimpleMessage : IMessage { public string GetMessage() => "Hello"; } public class HtmlDecorator : IMessage { private readonly IMessage _message; public HtmlDecorator(IMessage message) => _message = message; public string GetMessage() => $"<p>{_message.GetMessage()}</p>"; }Simple Explanation:
It’s like wrapping a gift. The gift is the same, but it now has a new appearance.
3. Composite Pattern
Use when: You want to treat individual objects and groups the same way.
public abstract class Component { public abstract void Display(); } public class Leaf : Component { private string name; public Leaf(string name) => this.name = name; public override void Display() => Console.WriteLine(name); } public class Composite : Component { private string name; private List<Component> children = new(); public Composite(string name) => this.name = name; public void Add(Component c) => children.Add(c); public override void Display() { Console.WriteLine(name); foreach (var child in children) child.Display(); } }Simple Explanation:
Like a folder that contains files and other folders. You treat all items the same.
🛋️ Behavioral Patterns
1. Observer Pattern
Use when: You want to notify multiple objects when something changes.
public interface IObserver { void Update(string message); } public class Subscriber : IObserver { private string name; public Subscriber(string name) => this.name = name; public void Update(string message) => Console.WriteLine($"{name} received: {message}"); } public class Publisher { private List<IObserver> observers = new(); public void Subscribe(IObserver observer) => observers.Add(observer); public void Notify(string msg) { foreach (var o in observers) o.Update(msg); } }Simple Explanation:
Like YouTube notifications. Subscribers get notified when a new video is uploaded.
2. Strategy Pattern
Use when: You want to choose an algorithm at runtime.
public interface ISortStrategy { void Sort(List<int> data); } public class QuickSort : ISortStrategy { public void Sort(List<int> data) => Console.WriteLine("Sorted using QuickSort"); } public class BubbleSort : ISortStrategy { public void Sort(List<int> data) => Console.WriteLine("Sorted using BubbleSort"); } public class Sorter { private ISortStrategy strategy; public Sorter(ISortStrategy strategy) => this.strategy = strategy; public void Sort(List<int> data) => strategy.Sort(data); }Simple Explanation:
It’s like choosing a route in Google Maps: fastest, shortest, or no tolls.
3. Mediator Pattern
Use when: You want to reduce direct communication between objects.
public interface IMediator { void Send(string message, Colleague sender); } public class ChatMediator : IMediator { private List<Colleague> users = new(); public void Register(Colleague user) => users.Add(user); public void Send(string message, Colleague sender) { foreach (var user in users.Where(u => u != sender)) user.Receive(message); } } public abstract class Colleague { protected IMediator mediator; public Colleague(IMediator mediator) => this.mediator = mediator; public abstract void Receive(string message); } public class User : Colleague { public string Name { get; } public User(IMediator mediator, string name) : base(mediator) => Name = name; public void Send(string message) => mediator.Send(message, this); public override void Receive(string message) => Console.WriteLine($"{Name} got: {message}"); }Simple Explanation:
Like a group chat: users don’t talk directly but through a central server.
👜 Repository Pattern (Bonus)
What It Is:
Abstracts your data access so the rest of the app doesn't know or care how the data is fetched.
Use When:
You want a clean separation between business logic and data access.
public interface IRepository<T> { IEnumerable<T> GetAll(); T GetById(int id); void Insert(T entity); void Update(T entity); void Delete(int id); void Save(); } public class ProductRepository : IRepository<Product> { private readonly MyDbContext _context; public ProductRepository(MyDbContext context) => _context = context; public IEnumerable<Product> GetAll() => _context.Products.ToList(); public Product GetById(int id) => _context.Products.Find(id); public void Insert(Product entity) => _context.Products.Add(entity); public void Update(Product entity) => _context.Products.Update(entity); public void Delete(int id) { var product = _context.Products.Find(id); if (product != null) _context.Products.Remove(product); } public void Save() => _context.SaveChanges(); }Simple Explanation:
It’s like a waiter in a restaurant. You don’t go to the kitchen; you ask the waiter to bring your food.
📚 Conclusion
Design patterns help developers create well-structured, scalable, and maintainable code. Start small with patterns like Singleton and Factory, then grow into advanced ones like Mediator or Repository. Learn them not just by reading, but by implementing them in your own projects.
public sealed class Logger { private static readonly Logger _instance = new Logger(); private Logger() {} public static Logger Instance => _instance; public void Log(string msg) => Console.WriteLine(msg);}