Every developer can recite what SOLID stands for, but in interviews and real-world reviews, it's the practical application that matters. Here’s my no-fluff cheatsheet with code smells, fixes, and analogies I use in reviews.
Want the PDF version? Click here to download
S: Single Responsibility Principle (SRP)
- Real Meaning: One reason to change, not one "thing" it does.
- Why It Matters: Avoids "God classes" that block clean PRs & slow refactoring.
- Personal Analogy: "If you can't give a clean commit message for the change, it's violating SRP."
- Code Smell: Method/class summary has multiple
and
/or
. - Actionable: Before adding a method, ask: "Is this a different concern?"
- Read more on SRP
- Short link: bytecrafted.dev/solid-srp
// Bad: Class doing too much (data + printing) public class Report { public string Content { get; set; } public void Print() => Console.WriteLine(Content); } // Good: Split responsibilities public class Report { public string Content { get; set; } } public class ReportPrinter { public void Print(Report r) => Console.WriteLine(r.Content); }
O: Open/Closed Principle (OCP)
- Real Meaning: Add features by extension, not by editing old code.
- Why It Matters: Keeps legacy code stable; new business rules plug in cleanly.
- Personal Analogy: "If a new requirement means touching brittle switch statements, you're not OCP."
- Code Smell: Growing
switch
/if
chains for types or behaviors. - Actionable: When adding a rule, prefer new handler/class over changing the old one.
- Read more on OCP
- Short link: bytecrafted.dev/solid-ocp
// Bad: Must edit method every time a new shape is added public double Area(object shape) { if (shape is Circle c) return Math.PI * c.Radius * c.Radius; if (shape is Square s) return s.Side * s.Side; return 0; } // Good: Add new shapes by extension public interface IShape { double Area(); } public class Circle : IShape { public double Radius { get; set; } public double Area() => Math.PI * Radius * Radius; } public class Square : IShape { public double Side { get; set; } public double Area() => Side * Side; }
L: Liskov Substitution Principle (LSP)
- Real Meaning: Subtypes must behave as expected, no surprises for callers.
- Why It Matters: Swapping implementations shouldn't break existing tests or runtime logic.
- Personal Analogy: "If a subclass throws where the base returns null, that's an LSP landmine."
- Code Smell: Derived classes override with different exceptions, parameters, or semantics.
- Actionable: Run parent class tests on every subclass; look for broken guarantees.
- Read more on LSP
- Short link: bytecrafted.dev/solid-lsp
// Bad: Subclass breaks expectations public class Bird { public virtual void Fly() { } } public class Penguin : Bird { public override void Fly() => throw new NotSupportedException(); } // Good: Refactor hierarchy public abstract class Bird { } public class FlyingBird : Bird { public void Fly() { /* ... */ } } public class Penguin : Bird { public void Swim() { /* ... */ } }
I: Interface Segregation Principle (ISP)
- Real Meaning: Small, client-focused interfaces, never force unused methods.
- Why It Matters: Reduces coupling, makes mocks/tests trivial, avoids NotSupportedException landmines.
- Personal Analogy: "If your interface summary needs bullet points, it's already too fat."
- Code Smell: Implementations with empty or
throw NotSupportedException
methods. - Actionable: Extract groups of related methods into separate interfaces as soon as a client skips one.
- Read more on ISP
- Short link: bytecrafted.dev/solid-isp
// Bad: Fat interface forces unused methods public interface IMachine { void Print(); void Scan(); void Fax(); } public class BasicPrinter : IMachine { public void Print() { } public void Scan() => throw new NotSupportedException(); public void Fax() => throw new NotSupportedException(); } // Good: Split into smaller interfaces public interface IPrinter { void Print(); } public interface IScanner { void Scan(); } public class BasicPrinter : IPrinter { public void Print() { } }
D: Dependency Inversion Principle (DIP)
- Real Meaning: Depend on abstractions, not concrete implementations, flip the usual control.
- Why It Matters: Makes business logic testable, swappable, and free of infrastructure glue.
- Personal Analogy: "If you see
new SqlRepo()
in a service, that's DIP going up in flames." - Code Smell: Direct instantiation of dependencies inside business logic.
- Actionable: Use constructor injection for every external dependency; mock in tests, swap in production.
- Read more on DIP
- Short link: bytecrafted.dev/solid-dip
// Bad: High-level depends on low-level directly public class ReportService { private readonly SqlReportRepo _repo = new SqlReportRepo(); } // Good: Depend on abstraction, inject implementation public interface IReportRepo { /* ... */ } public class SqlReportRepo : IReportRepo { /* ... */ } public class ReportService { private readonly IReportRepo _repo; public ReportService(IReportRepo repo) { _repo = repo; } }
Read full series: bytecrafted.dev/series/solid.
Top comments (0)