Design Patterns: SOLID Principles
Master SOLID principles for maintainable, flexible code.
A Simple Analogy
SOLID is like good architecture for buildings. Each principle ensures structures are strong, flexible, and easy to modify without collapse.
SOLID Principles
| Letter | Principle | Meaning | |--------|-----------|---------| | S | Single Responsibility | One reason to change | | O | Open/Closed | Open for extension, closed for modification | | L | Liskov Substitution | Subtypes must substitute base types | | I | Interface Segregation | Many specific interfaces over generic ones | | D | Dependency Inversion | Depend on abstractions, not concrete implementations |
Single Responsibility Principle
// Bad: Multiple responsibilities
public class OrderService
{
public void CreateOrder(Order order) { }
public void SendEmail(string email) { }
public void SaveToDatabase(Order order) { }
public void GenerateInvoice(Order order) { }
}
// Good: Separated concerns
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailService _emailService;
private readonly IInvoiceGenerator _invoiceGenerator;
public async Task CreateOrderAsync(Order order)
{
await _repository.SaveAsync(order);
await _emailService.SendOrderConfirmation(order);
var invoice = _invoiceGenerator.Generate(order);
}
}
Open/Closed Principle
// Bad: Must modify class for new types
public class ReportGenerator
{
public string Generate(string type)
{
return type switch
{
"pdf" => GeneratePdf(),
"excel" => GenerateExcel(),
"csv" => GenerateCsv(),
_ => throw new InvalidOperationException()
};
}
}
// Good: Open for extension, closed for modification
public interface IReportRenderer
{
string Render(ReportData data);
}
public class ReportGenerator
{
private readonly IReportRenderer _renderer;
public string Generate(ReportData data)
{
return _renderer.Render(data); // Works with any renderer
}
}
// Add new type without modifying existing code
public class XmlReportRenderer : IReportRenderer
{
public string Render(ReportData data) => GenerateXml(data);
}
Liskov Substitution Principle
// Bad: Derived class breaks contract
public class PaymentProcessor
{
public virtual decimal CalculateFee(decimal amount) => amount * 0.02m;
}
public class CreditCardProcessor : PaymentProcessor
{
public override decimal CalculateFee(decimal amount) => 0; // Breaks LSP!
}
// Good: Derived class honors contract
public interface IPaymentProcessor
{
decimal CalculateFee(decimal amount);
}
public class CreditCardProcessor : IPaymentProcessor
{
public decimal CalculateFee(decimal amount) => amount * 0.02m;
}
public class CryptoCurrencyProcessor : IPaymentProcessor
{
public decimal CalculateFee(decimal amount) => amount * 0.005m;
}
// Both can substitute for IPaymentProcessor
Interface Segregation Principle
// Bad: Fat interface, clients depend on unused methods
public interface IRepository<T>
{
void Create(T item);
void Update(T item);
void Delete(T item);
T GetById(int id);
List<T> GetAll();
void BulkInsert(List<T> items);
void BulkDelete(List<int> ids);
void Archive(int id);
}
// Good: Segregated interfaces
public interface IReadRepository<T>
{
T GetById(int id);
List<T> GetAll();
}
public interface IWriteRepository<T>
{
void Create(T item);
void Update(T item);
void Delete(T item);
}
public interface IBulkRepository<T>
{
void BulkInsert(List<T> items);
void BulkDelete(List<int> ids);
}
// Client only depends on what it needs
public class OrderService
{
private readonly IReadRepository<Order> _reader;
private readonly IWriteRepository<Order> _writer;
}
Dependency Inversion Principle
// Bad: High-level depends on low-level
public class OrderService
{
private readonly SqlServerRepository _repository;
public OrderService()
{
_repository = new SqlServerRepository();
}
}
// Good: Both depend on abstraction
public interface IOrderRepository
{
Task<Order> GetAsync(int id);
Task SaveAsync(Order order);
}
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
}
// Can use any repository implementation
Practical Example
// Applying all SOLID principles
public interface IOrderValidator { Task ValidateAsync(Order order); }
public interface IOrderRepository { Task SaveAsync(Order order); }
public interface IEmailService { Task SendAsync(string to, string subject); }
public interface IInvoiceGenerator { Invoice Generate(Order order); }
public class OrderService : IOrderService // S: Single responsibility
{
private readonly IOrderValidator _validator; // D: Depend on abstraction
private readonly IOrderRepository _repository;
private readonly IEmailService _emailService;
public async Task CreateAsync(Order order) // I: Interfaces segregated
{
await _validator.ValidateAsync(order); // O: Easy to add validators
await _repository.SaveAsync(order);
await _emailService.SendAsync(order.Email, "Order Confirmed");
}
}
Best Practices
- Apply gradually: Don't over-engineer early
- Use SOLID as guide: Not strict rules
- Refactor when needed: Improve as complexity grows
- Test-driven design: Tests reveal coupling issues
- Code reviews: Catch violations early
Related Concepts
- Design patterns (Observer, Factory, etc.)
- Composition over inheritance
- Interface-based design
- Test-driven development
Summary
SOLID principles create maintainable, flexible code. Master each principle to design systems that adapt to change without breaking existing functionality.