Isaac.

Design Patterns: SOLID Principles

Master SOLID principles for maintainable, flexible code.

By EMEPublished: February 20, 2025
design patternssolidarchitecturecode quality

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

  1. Apply gradually: Don't over-engineer early
  2. Use SOLID as guide: Not strict rules
  3. Refactor when needed: Improve as complexity grows
  4. Test-driven design: Tests reveal coupling issues
  5. 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.