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.