Ever spent a whole weekend untangling a monster “God class,” only to discover you’ve accidentally spawned three more? I’ve been there—coffee-fueled, eyes bleary, CTRL + F’ing through 5,000‑line files wondering how future‑me allowed past‑me to ship that spaghetti. The good news? You can avoid that déjà‑vu maintenance nightmare by baking in SOLID principles from day one. Let’s turn that codebase of doom into something you’ll actually enjoy opening on a Monday morning.
Single Responsibility Principle (SRP)
“A class should have one, and only one, reason to change.” — Robert C. Martin
Why you should care
- Faster bug‑fixes & clearer intent. Smaller classes = tighter feedback loops.
- Easier onboarding. New teammates grasp intent without playing detective.
Common smell
If you spot methods with verbs from different domains—SendEmail()
, CalculateTax()
, SaveToDb()
—inside the same class, SRP is waving at you.
Code example
// BEFORE: Violates SRP – one class does billing and persistence
public class InvoiceService
{
public decimal CalculateTotal(IEnumerable<InvoiceLine> lines) { /* … */ }
public void Save(Invoice invoice) { /* writes to SQL */ }
}
// AFTER: Each class owns one job
public class InvoiceCalculator
{
public decimal CalculateTotal(IEnumerable<InvoiceLine> lines)
=> lines.Sum(l => l.Price * l.Quantity);
}
public interface IInvoiceRepository
{
void Save(Invoice invoice);
}
public class SqlInvoiceRepository : IInvoiceRepository
{
public void Save(Invoice invoice)
{
// Saving logic
}
}
Open/Closed Principle (OCP)
“Software entities should be open for extension, but closed for modification.”
The gist
Add new behavior without editing existing, tested code.
Code example: Dynamic discounts
public interface IDiscountRule
{
decimal Apply(Customer c, decimal price);
}
public class LoyaltyRule : IDiscountRule
{
public decimal Apply(Customer c, decimal price)
=> c.IsLoyal ? price * 0.9m : price;
}
public class BlackFridayRule : IDiscountRule
{
public decimal Apply(Customer c, decimal price)
=> DateTime.UtcNow.Month == 11 ? price * 0.8m : price;
}
public class PricingService
{
private readonly IEnumerable<IDiscountRule> _rules;
public PricingService(IEnumerable<IDiscountRule> rules) => _rules = rules;
public decimal Calculate(Customer c, decimal basePrice)
=> _rules.Aggregate(basePrice, (price, rule) => rule.Apply(c, price));
}
Need a “Student” promotion next month? Ship a new StudentRule
class—no touching core logic.
Liskov Substitution Principle (LSP)
“Objects of a superclass should be replaceable with objects of a subclass without breaking the application.”
The classic pitfall
Square
inherits from Rectangle
, overrides Width
& Height
, and then your UI layout code implodes.
Safer design
public interface IShape
{
int Area();
}
public class Rectangle : IShape
{
public int Width { get; }
public int Height { get; }
public Rectangle(int w, int h) { Width = w; Height = h; }
public int Area() => Width * Height;
}
public class Square : IShape
{
public int Side { get; }
public Square(int side) => Side = side;
public int Area() => Side * Side;
}
Both classes satisfy the same contract (Area()
), so downstream algorithms remain blissfully unaware of shape specifics.
Interface Segregation Principle (ISP)
“Clients should not be forced to depend upon interfaces they do not use.”
Real‑world analogy
Think Swiss Army knives: handy in the woods, painful when you just need a single screwdriver bit.
Code example: Modular printing
public interface IPrinter
{
void Print(Document doc);
}
public interface IScanner
{
void Scan(Document doc);
}
public class AllInOne : IPrinter, IScanner { /* … */ }
public class ThermalPrinter : IPrinter { /* … */ }
The shipping label microservice happily references IPrinter
only—no compile‑time baggage from scanning APIs it will never call.
Dependency Inversion Principle (DIP)
“High‑level modules should not depend on low‑level modules. Both should depend on abstractions.”
The pain it fixes
Tight‑coupled code makes swapping an SMTP library feel like performing open‑heart surgery on your dashboard.
Code example: Notification service
public interface IEmailService
{
Task SendAsync(string to, string subject, string body);
}
public class SmtpEmailService : IEmailService { /* … */ }
public class SendGridEmailService : IEmailService { /* … */ }
public class OrderController
{
private readonly IEmailService _mailer;
public OrderController(IEmailService mailer) => _mailer = mailer;
public async Task ConfirmAsync(Order order)
{
// business logic...
await _mailer.SendAsync(order.CustomerEmail,
"Order Confirmation",
$"Thanks for buying #{order.Id}");
}
}
Switching from raw SMTP to SendGrid became a 15‑minute dependency‑injection tweak instead of a two‑day refactor.
FAQ: SOLID in Everyday .NET Projects
They may add minutes today but save days next quarter. Think of it as paying technical debt upfront at 0 % interest.
Absolutely—guidelines, not handcuffs. Just document why and revisit later.
Beautifully. Records shine for value objects under SRP; minimal APIs remain testable when wired through abstractions that honor DIP.
Try Roslyn analyzers (e.g., StyleCop), NDepend dashboards, and Architecture Decision Records (ADRs) for capturing intent.
Conclusion: Keep Your Codebase SOLID—Literally
Mastering SOLID isn’t academic chest‑thumping; it’s the secret sauce that lets you ship features faster, squash bugs sooner, and onboard new devs without handing them a map of pitfalls. Challenge yourself: pick one principle this week—maybe refactor that bloated service into lean, SRP‑aligned classes—and feel the maintenance friction melt away.
Question for you: Which SOLID principle trips you up most often, and what have you tried to tame it? Drop a comment—I read every single one.
Isn’t SRP too restrictive? What if my class needs to do slightly more than one thing?
SRP doesn’t restrict a class to just one function, but to one responsibility.
So If multiple functions serve the same end goal, they can still align with SRP.