GB.
2024-06-035 min read

OOP Design Patterns: The Ultimate Guide for Beginners

#OOP#Design Patterns#C##Software Architecture#Beginner Friendly#Clean Code

OOP Design Patterns: The Ultimate Guide for Beginners

You've learned the four pillars of OOP. Now what? Design Patterns are proven solutions to common programming problems. Let's explore the most important ones.


1. Singleton Pattern - The One and Only

What it is: Ensures a class has only one instance.

Analogy: The President of a country. There can only be one at a time.

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

    private DatabaseConnection() { }

    public static DatabaseConnection GetInstance()
    {
        if (_instance == null)
        {
            lock (_lock)
            {
                if (_instance == null) _instance = new DatabaseConnection();
            }
        }
        return _instance;
    }
}

Use when: Database connections, configuration settings, logging services.


2. Factory Pattern - The Object Creator

What it is: Defines an interface for creating objects, but lets subclasses decide which class to instantiate.

Analogy: A car factory produces sedans, SUVs, or trucks based on demand.

public interface INotification { void Send(string message); }
public class EmailNotification : INotification { public void Send(string msg) { } }
public class SmsNotification : INotification { public void Send(string msg) { } }

public class NotificationFactory
{
    public static INotification Create(string type)
    {
        return type.ToLower() switch
        {
            "email" => new EmailNotification(),
            "sms" => new SmsNotification(),
            _ => throw new ArgumentException("Invalid type")
        };
    }
}

// Usage
var notification = NotificationFactory.Create("email");
notification.Send("Hello!");

Use when: Creating objects without specifying exact class, complex object creation.


3. Observer Pattern - The Event System

What it is: When one object changes state, all its dependents are notified automatically.

Analogy: YouTube subscriptions. When a creator uploads, all subscribers get notified.

public interface IObserver { void Update(decimal price); }

public class StockPrice
{
    private List<IObserver> _observers = new List<IObserver>();
    private decimal _price;

    public void Attach(IObserver observer) => _observers.Add(observer);
    public void Detach(IObserver observer) => _observers.Remove(observer);

    public void UpdatePrice(decimal newPrice)
    {
        _price = newPrice;
        foreach (var observer in _observers) observer.Update(_price);
    }
}

Use when: Event handling, UI updates, real-time notifications.


4. Strategy Pattern - The Interchangeable Algorithm

What it is: Defines a family of algorithms and makes them interchangeable.

Analogy: GPS navigation with fastest, shortest, or scenic routes. Same destination, different strategies.

public interface IDiscountStrategy
{
    decimal CalculateDiscount(Order order);
}

public class PercentageDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(Order order) => order.Total * 0.1m;
}

public class FixedDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(Order order) => 10;
}

public class OrderCalculator
{
    private readonly IDiscountStrategy _discountStrategy;

    public OrderCalculator(IDiscountStrategy discountStrategy)
    {
        _discountStrategy = discountStrategy;
    }

    public decimal CalculateDiscount(Order order) => _discountStrategy.CalculateDiscount(order);
}

Use when: Multiple algorithms for the same task, need to switch algorithms at runtime.


5. Decorator Pattern - The Layer Cake

What it is: Dynamically adds new functionality to objects without modifying their structure.

Analogy: A plain coffee. You can add milk, sugar, whipped cream. Each addition wraps the previous one.

public interface ICoffee
{
    decimal GetCost();
    string GetDescription();
}

public class PlainCoffee : ICoffee
{
    public decimal GetCost() => 2.00m;
    public string GetDescription() => "Plain coffee";
}

public abstract class CoffeeDecorator : ICoffee
{
    protected ICoffee _coffee;
    public CoffeeDecorator(ICoffee coffee) { _coffee = coffee; }
    public virtual decimal GetCost() => _coffee.GetCost();
    public virtual string GetDescription() => _coffee.GetDescription();
}

public class MilkDecorator : CoffeeDecorator
{
    public MilkDecorator(ICoffee coffee) : base(coffee) { }
    public override decimal GetCost() => base.GetCost() + 0.50m;
    public override string GetDescription() => base.GetDescription() + ", milk";
}

// Usage
ICoffee coffee = new MilkDecorator(new PlainCoffee());
// coffee.GetDescription() => "Plain coffee, milk"

Use when: Adding responsibilities dynamically, when subclassing is impractical.


6. Adapter Pattern - The Universal Converter

What it is: Converts the interface of a class into another interface clients expect.

Analogy: A power adapter. Your American plug doesn't fit European outlets, so you use an adapter.

// Old system
public class LegacyPaymentProcessor
{
    public void ProcessPaymentLegacy(decimal amount, string currency) { }
}

// New interface
public interface IPaymentProcessor
{
    void ProcessPayment(PaymentRequest request);
}

// Adapter
public class PaymentAdapter : IPaymentProcessor
{
    private readonly LegacyPaymentProcessor _legacyProcessor;

    public PaymentAdapter(LegacyPaymentProcessor legacyProcessor)
    {
        _legacyProcessor = legacyProcessor;
    }

    public void ProcessPayment(PaymentRequest request)
    {
        _legacyProcessor.ProcessPaymentLegacy(request.Amount, request.Currency);
    }
}

Use when: Integrating third-party libraries, working with legacy systems.


Quick Reference

PatternAnalogyUse Case
SingletonPresidentOne instance only
FactoryCar factoryObject creation
ObserverYouTube subscriptionsEvent systems
StrategyGPS routesMultiple algorithms
DecoratorCoffee customizerExtending functionality
AdapterPower adapterIntegration

Common Mistakes to Avoid

  1. Overusing patterns - Not every problem needs a pattern
  2. Ignoring patterns - Reinventing the wheel is wasteful
  3. Wrong pattern - Using Singleton when you need multiple instances

The Golden Rule

Keep it simple. Don't use a pattern if a simple solution works. Use patterns to address actual challenges.


Happy coding - and may your code be always well-patterned and your bugs always easy to find!

Enjoyed this article? Share it with your network!