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.