🧠 Why SOLID Principles?
In software development, the real challenge is maintainability. Most systems fail not because they don't work—but because they are hard to evolve. SOLID principles are design rules to keep your code clean, extendable, and testable.
Imagine trying to upgrade a house where all rooms share the same electric switchboard. One change and the entire house breaks! SOLID helps you decouple systems and design for change.
🔵 1. Single Responsibility Principle (SRP)
“A class should have only one reason to change.”
🧩 Simple Explanation
A class should do one thing, and do it well. It should have only one responsibility, like a well-defined job role.
🧠 Real-life Analogy
In a restaurant:
- The Chef cooks,
- The Waiter serves,
- The Manager manages operations.
You don’t expect the Chef to also handle billing. Same with classes.
🚫 Common Violation:
class InvoiceManager { public void GenerateInvoice() { /* logic */ } public void SaveToFile() { /* file I/O */ } public void EmailInvoice() { /* SMTP */ } }
This class:
- Handles invoice logic
- Handles persistence
- Sends emails
✅ Refactored with SRP:
class Invoice { public string Generate() => "Invoice Content"; } class FileWriter { public void Save(string content) { /* write to disk */ } } class EmailSender { public void Send(string content) { /* send email */ } }
Each class now has a single responsibility, and changes affect only one class.
🟠 2. Open/Closed Principle (OCP)
“Software entities should be open for extension, but closed for modification.”
🧠 In Simple Terms:
You should be able to add new behavior without changing existing, tested code.
🔧 Problem:
Adding new logic often tempts developers to modify old classes.
🚫 Bad OCP:
class Shape { public string Type; } class AreaCalculator { public double Calculate(Shape shape) { if (shape.Type == "Circle") { /*...*/ } else if (shape.Type == "Rectangle") { /*...*/ } return 0; } }
Each time a new shape is added, we must modify AreaCalculator.
✅ Good OCP (Polymorphism):
abstract class Shape { public abstract double CalculateArea(); } class Circle : Shape { public double Radius { get; set; } public override double CalculateArea() => Math.PI * Radius * Radius; } class Rectangle : Shape { public double Width { get; set; } public double Height { get; set; } public override double CalculateArea() => Width * Height; }
Now we can extend new shapes without changing the existing logic.
🟡 3. Liskov Substitution Principle (LSP)
“Derived types must be substitutable for their base types.”
🧠 What it means:
Anywhere a base class is used, you should be able to use its subclass without surprises.
🚫 LSP Violation Example:
class Bird { public virtual void Fly() => Console.WriteLine("Flying"); } class Ostrich : Bird { public override void Fly() => throw new NotSupportedException("I can't fly"); }
Passing Ostrich into a method expecting a Bird breaks the contract.
✅ LSP-Compliant Design:
abstract class Bird { } interface IFlyingBird { void Fly(); } class Sparrow : Bird, IFlyingBird { public void Fly() => Console.WriteLine("Flying"); } class Ostrich : Bird { }
Use interfaces or behavioral subtyping to separate expectations.
🟢 4. Interface Segregation Principle (ISP)
“Clients should not be forced to depend on interfaces they do not use.”
🧠 Meaning:
Split large interfaces into smaller, more specific ones.
🧠 Analogy:
If you're using a remote control, you shouldn't be forced to implement buttons for Netflix if you only want to turn the TV on.
🚫 ISP Violation:
interface IMachine { void Print(); void Scan(); void Fax(); } class OldPrinter : IMachine { public void Print() { } public void Scan() => throw new NotImplementedException(); public void Fax() => throw new NotImplementedException(); }
Why implement what we don’t need?
✅ ISP-Compliant Design:
interface IPrinter { void Print(); } interface IScanner { void Scan(); } class SimplePrinter : IPrinter { public void Print() { } } class MultiFunctionPrinter : IPrinter, IScanner { public void Print() { } public void Scan() { } }
Now devices can choose what features they need to support.
🔴 5. Dependency Inversion Principle (DIP)
“High-level modules should not depend on low-level modules. Both should depend on abstractions.”
🧠 What It Means:
Instead of directly depending on concrete implementations, depend on interfaces or abstractions.
🚫 DIP Violation:
class SqlLogger { public void Log(string msg) { /* SQL write */ } } class OrderProcessor { private SqlLogger logger = new SqlLogger(); public void Process() => logger.Log("Order processed"); }
You can’t test or switch logging easily.
✅ DIP Solution:
interface ILogger { void Log(string message); } class SqlLogger : ILogger { public void Log(string msg) { } } class FileLogger : ILogger { public void Log(string msg) { } } class OrderProcessor { private readonly ILogger _logger; public OrderProcessor(ILogger logger) { _logger = logger; } public void Process() => _logger.Log("Order processed"); }
Now we can easily swap loggers or inject mocks during tests.
🚀 Advanced Applications of SOLID
🔄 In ASP.NET Core:
- Controllers: SRP (only handle HTTP)
- Services/Handlers: OCP (extend with new business logic)
- Middlewares: LSP/ISP in filters and pipelines
- DI Container: DIP is automatically enforced
🧪 Testing:
- SRP and DIP make unit testing straightforward.
- ISP ensures mocks don't require irrelevant methods.
- OCP avoids changing tests when you add new types.
🎯 Design Patterns Connection:
| SOLID Principle | Related Patterns |
|---|---|
| SRP | Command, Builder |
| OCP | Strategy, Decorator |
| LSP | Template Method, State |
| ISP | Adapter, Proxy |
| DIP | Dependency Injection, Bridge |
✅ Conclusion
The SOLID principles are foundations of clean, scalable, and maintainable code. They are not rigid rules, but powerful guidelines that:
- Improve collaboration in teams
- Future-proof your applications
- Make testing and debugging easier
- Help you think in components rather than blobs of code
Mastering SOLID takes practice, but it pays off in every line you write.
🔖 Bonus Tip:
Start with SRP and DIP. These two alone can transform your codebase.