Design Patterns & Practices  

SOLID Principles in Software Design (With C# Examples)

Introduction

The SOLID principles are five design principles that make software easier to understand, maintain, and extend. They help you create systems that are modular, flexible, and decoupled.

Below is a structured breakdown of each principle with practical C# examples.

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.

Why it’s important:
If a class does too many things, any change in one responsibility may affect other behaviors, making the system harder to maintain and test.

In practice: If a class handles both user authentication and logging, it violates SRP. These responsibilities should be separated.

Violation of SRP:

public class UserManager
{
    public void RegisterUser(string username, string password) { ... }
    public void Log(string message) { ... } // Logging should be separate
}

Compliant with SRP:

public interface ILogger
{
    void Log(string message);
}

public class UserManager
{
    private readonly ILogger _logger;

    public UserManager(ILogger logger)
    {
        _logger = logger;
    }

    public void RegisterUser(string username, string password)
    {
        // Registration logic
    }
}

public class Logger : ILogger
{
    public void Log(string message) { ... }
}

2. Open/Closed Principle (OCP)

Definition: Software entities should be open for extension but closed for modification.

Why it’s important: You should extend behavior without modifying existing code, reducing the risk of introducing new bugs.

Violation of OCP:

public class PaymentProcessor
{
    public void ProcessPayment(PaymentType type)
    {
        if (type == PaymentType.CreditCard) { ... }
        else if (type == PaymentType.PayPal) { ... }
        // Adding new methods requires modifying this class
    }
}

Compliant with OCP:

public interface IPaymentMethod
{
    void ProcessPayment();
}

public class CreditCardPayment : IPaymentMethod
{
    public void ProcessPayment() { ... }
}

public class PayPalPayment : IPaymentMethod
{
    public void ProcessPayment() { ... }
}

public class PaymentProcessor
{
    private readonly IPaymentMethod _paymentMethod;

    public PaymentProcessor(IPaymentMethod paymentMethod)
    {
        _paymentMethod = paymentMethod;
    }

    public void ProcessPayment()
    {
        _paymentMethod.ProcessPayment();
    }
}

3. Liskov Substitution Principle (LSP)

Definition: Objects of a superclass should be replaceable with objects of a subclass without breaking application behavior.

Why it’s important: Derived classes must honor the behavior expected from the base class.

Violation of LSP:

public class Bird
{
    public virtual void Fly() { ... }
}

public class Ostrich : Bird
{
    public override void Fly()
        => throw new InvalidOperationException("Cannot fly");
}

Compliant with LSP:

public class Bird
{
    public virtual void Move() { ... }
}

public class Sparrow : Bird
{
    public override void Move() { Fly(); }
    public void Fly() { ... }
}

public class Ostrich : Bird
{
    public override void Move() { Run(); }
    public void Run() { ... }
}

4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on interfaces they do not use.

Why it’s important: Large, “fat” interfaces cause unnecessary implementation requirements.

Example: Instead of one large interface:

public interface IPaymentService
{
    void Validate();
    void Process();
    void GenerateReport();
}

Break it into smaller interfaces:

public interface IPaymentValidator
{
    void Validate();
}

public interface IPaymentProcessor
{
    void Process();
}

public interface IPaymentReporter
{
    void GenerateReport();
}

Classes implement only what they need.

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Why it’s important:
It reduces tight coupling and increases flexibility.

Violation of DIP:

public class DatabaseService
{
    private SqlConnection _connection;

    public DatabaseService()
    {
        _connection = new SqlConnection();
    }
}

Compliant with DIP:

public interface IDatabaseConnection
{
    void Connect();
}

public class SqlConnection : IDatabaseConnection
{
    public void Connect() { ... }
}

public class DatabaseService
{
    private readonly IDatabaseConnection _connection;

    public DatabaseService(IDatabaseConnection connection)
    {
        _connection = connection;
    }
}

Why SOLID Matters

Flexibility: Extend systems without breaking existing functionality.

Maintainability: Code becomes easier to manage as responsibilities are clearly separated.

Testability: Decoupled components can be independently unit tested.

Real-World Example in C# (E-commerce System)

If you’re building an e-commerce system:

  • Use SRP to separate Order Processing and Payment Processing.

  • Use OCP to add new payment methods without modifying the core processor.

  • Follow LSP to ensure all payment types behave consistently.

  • Apply ISP to split validation, processing, and reporting into separate interfaces.

  • Use DIP so your system depends on payment abstractions, not specific APIs.

By following SOLID principles, you build software that is scalable, maintainable, extensible, and robust for long-term growth.