Design Patterns

Intermediate 25 min read Lesson 3 of 6

Design patterns are proven, reusable solutions to common software design problems. Here are the ones you'll encounter most as a C# developer.

Singleton — exactly one instance

public sealed class AppLogger
{
    private static readonly Lazy<AppLogger> _instance = new(() => new AppLogger());
    public static AppLogger Instance => _instance.Value;

    private AppLogger() { }

    public void Log(string message) => Console.WriteLine($"[LOG] {message}");
}

// Usage:
AppLogger.Instance.Log("Application started");

Factory — centralize object creation

public interface INotification
{
    void Send(string message);
}

public class EmailNotification : INotification
{
    public void Send(string message) => Console.WriteLine($"Email: {message}");
}

public class SmsNotification : INotification
{
    public void Send(string message) => Console.WriteLine($"SMS: {message}");
}

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

Repository — separate data access from business logic

public interface IStudentRepository
{
    Student? GetById(int id);
    void Add(Student student);
}

public class SqlStudentRepository : IStudentRepository
{
    // ADO.NET or EF Core implementation here
    public Student? GetById(int id) { /* ... */ return null; }
    public void Add(Student student) { /* ... */ }
}

This lets you swap the data source without touching the rest of your app.

Dependency Injection — don't build your own dependencies

public class StudentService
{
    private readonly IStudentRepository _repository;

    public StudentService(IStudentRepository repository)
    {
        _repository = repository;
    }
}

// In Program.cs:
builder.Services.AddScoped<IStudentRepository, SqlStudentRepository>();
builder.Services.AddScoped<StudentService>();

Strategy — swap behavior at runtime

public interface IDiscountStrategy
{
    decimal Apply(decimal price);
}

public class StudentDiscount : IDiscountStrategy
{
    public decimal Apply(decimal price) => price * 0.9m;
}

public class NoDiscount : IDiscountStrategy
{
    public decimal Apply(decimal price) => price;
}

// Usage:
IDiscountStrategy strategy = isStudent ? new StudentDiscount() : new NoDiscount();
decimal finalPrice = strategy.Apply(100m);
Key Takeaway

Design patterns are reusable solutions that make your code more maintainable and flexible.

Test Your Knowledge - Take Quiz