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.
Related Articles
CQRS and Event Sourcing
Separate reads and writes with CQRS and Event Sourcing patterns.
Read More design-patternsSOLID Principles
Master SOLID design principles for maintainable, scalable code.
Read More infrastructureApache Kafka Streaming
Learn about Apache Kafka and event streaming architecture.
Read More