SOLID Principles
SOLID Principles Demystified: Examples, Conflicts, and ‘Wait, That’s the Opposite?’
You know SOLID. Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion.
But then you try to apply them and hit walls:
- “Isn’t Interface Segregation just Single Responsibility for interfaces?”
- “Open/Closed says no modification, but I have to modify to extend… huh?”
- “Dependency Inversion - why should high-level modules depend on abstractions? That feels backwards.”
Let’s clear it up with real examples and show where principles conflict (and how to resolve them).
S - Single Responsibility Principle (SRP)
A class should have one reason to change.
Example - Bad:
public class Invoice
{
public decimal CalculateTotal() { ... }
public void SaveToDatabase() { ... }
public void SendEmail() { ... }
}
Three reasons to change: tax logic, DB schema, email formatting.
Example - Good:
public class InvoiceCalculator { ... }
public class InvoiceRepository { ... }
public class InvoiceNotifier { ... }
Common confusion: “What counts as a reason?”
Tip: A reason = an actor (accounting, DB admin, operations). If different people ask for changes, separate.
Conflict with Open/Closed?
SRP often leads to many small classes. That’s fine. OCP then says you can extend them without changing them.
O - Open/Closed Principle (OCP)
Open for extension, closed for modification.
Example - Violation (adding new report type modifies existing code):
public class ReportGenerator
{
public string Generate(string type)
{
if (type == "PDF") return "PDF...";
else if (type == "Excel") return "Excel...";
// adding CSV? modify this method.
}
}
Example - OCP compliant (Strategy pattern):
public interface IReportGenerator { string Generate(); }
public class PdfGenerator : IReportGenerator { ... }
public class ExcelGenerator : IReportGenerator { ... }
Biggest confusion: “But I still modify the DI container registration - isn’t that modification?”
Tip: OCP refers to source code of the existing class that consumers depend on. Adding new implementations doesn’t change the existing ReportGenerator class - you just plug in a new one.
Conflict with YAGNI (You Ain’t Gonna Need It):
Making everything OCP up front leads to over-engineering. Apply OCP when you know variations are likely (e.g., payment methods). Otherwise, start simple and refactor when the second variation appears.
L - Liskov Substitution Principle (LSP)
Derived classes must be substitutable for their base classes without breaking behavior.
Example - Broken:
public class Bird { public virtual void Fly() { } }
public class Penguin : Bird { public override void Fly() => throw new Exception("Can't fly"); }
Calling Bird.Fly() on a penguin breaks.
Example - Fix:
public abstract class Bird { }
public interface IFlyable { void Fly(); }
public class Sparrow : Bird, IFlyable { ... }
public class Penguin : Bird { }
Common confusion: “My derived class adds extra validation - that’s fine, right?”
Tip: It’s fine only if base class contracts allow it. If base says Save() never throws a ValidationException, your derived class cannot introduce it.
Conflict with OCP?
No - LSP is about behavioral contract, OCP about structural extension. Both want you to use abstractions.
I - Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they don’t use.
Example - Fat interface:
public interface IWorker
{
void Work();
void Eat();
void Sleep();
}
public class Robot : IWorker { void Work() { ... } void Eat() => throw new NotImplemented(); ... }
Segregated:
public interface IWorkable { void Work(); }
public interface IEatable { void Eat(); }
public class Human : IWorkable, IEatable { ... }
public class Robot : IWorkable { ... }
Common confusion: “ISP is just SRP for interfaces.”
Not exactly. SRP is about reasons to change (high cohesion within one class). ISP is about avoiding pollution (clients shouldn’t see irrelevant methods). They often align but not always - e.g., a printer interface with Print() and Scan() might be fine for a multi‑function device (one client), but separate for a basic printer client.
Conflict with DRY?
ISP may cause duplicate interface definitions (e.g., IWorkable and ITask). That’s okay - duplicate interface names are cheap; dependency pollution is expensive.
D - Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Example - Violation:
public class EmailNotifier
{
private SmtpClient _smtp = new SmtpClient(); // low-level concrete
public void Send(string msg) { _smtp.Send(msg); }
}
Example - DIP:
public interface IMessageSender { void Send(string msg); }
public class SmtpSender : IMessageSender { ... }
public class EmailNotifier
{
private readonly IMessageSender _sender;
public EmailNotifier(IMessageSender sender) => _sender = sender;
}
Common confusion: “Isn’t that just Dependency Injection?”
Dependency Injection (DI) is a mechanism (constructor injection). DIP is a principle (direction of dependency). You can use DI without DIP (inject a concrete class - still flexible but still depends on low-level). DIP says depend on abstractions.
Conflict with OCP?
None - they work together: DIP supplies the abstraction, OCP says you can extend it without modifying high-level code.
The Ultimate Confusion Matrix
| Principle | Short definition | Most misunderstood as | Typical conflict |
|---|---|---|---|
| SRP | One reason to change | “One method per class” | OCP (too many classes? no) |
| OCP | Extend without modifying | “Never modify anything” | YAGNI (over‑engineering) |
| LSP | Subtypes must behave | “Same interface = same behavior” | None - but often broken by inheritance misuse |
| ISP | Don’t force unused methods | “SRP for interfaces” | DRY (duplicate interfaces) |
| DIP | Depend on abstractions | “Just use DI container” | None - but beginners think it’s overkill |
One Rule of Thumb to Rule Them All
If a change forces you to modify many unrelated classes - you broke SOLID.
If a new feature only requires adding new classes (not changing existing ones) - you’re doing it right.
Don’t try to apply all five on day one.
Start with SRP and DIP. Add OCP when you see a second variation. Apply ISP when interfaces become fat. Fix LSP when your unit tests start failing in weird ways.
Quick Tips for Interviews or Code Reviews
-
“How is ISP different from SRP?”
SRP = class cohesion. ISP = interface pollution. A class can follow SRP but implement a fat interface - that’s an ISP violation. -
“Give me a real LSP violation example.”
Squareinheriting fromRectangleand overridingWidthandHeightindependently - breaks because settingWidthon a square shouldn’t keepHeightunchanged. -
“Can you violate DIP without using
new?”
Yes - callingstaticmethods orDateTime.Now(concrete system dependency) is a DIP violation. Inject an abstractionITimeProvider. -
“Should every interface have exactly one method?”
No. ISP says don’t force methods on clients that don’t need them. If a client needs 3 related methods, put them together.
Happy coding - and may your dependencies always point inward!
Enjoyed this article? Share it with your network!