SOLID: Five Rules That Keep Code Changeable
Why adding a feature should mean adding code, not editing five old files
Guess first
Pick an answer. You will see if you were right at the end of the lesson.
OrderService has a switch on discount type. A new discount type arrives. What does Open/Closed suggest?
Real-world analogy
What is it?
SOLID is five design rules for object-oriented code: Single responsibility, Open/Closed, Liskov substitution, Interface segregation and Dependency inversion. They all aim at one thing: a change in one place should not force changes in many places. They are guidance learned from painful codebases, not a checklist to apply everywhere.
Real-world relevance
A shop starts with one discount and a switch in OrderService. A year later there are twelve discount types and every new one risks breaking the old ones. Moving each discount into its own IDiscountRule class means a new promotion is one new file, with its own tests.
Key points
- S: one reason to change — The Single Responsibility Principle says a class should have one reason to change. A class that calculates tax and also sends email changes for two unrelated reasons.
- O: extend, do not edit — The Open/Closed Principle means you add a new class instead of a new if in old code. A switch on discount type grows forever, while a list of IDiscountRule objects just gets one more item.
- L: subtypes keep the promise — The Liskov Substitution Principle says a child must work wherever its parent is used. A ReadOnlyCollection passed as an IList throws on Add, so callers expecting a normal list crash.
- Square is not a Rectangle — If Square inherits Rectangle, setting width also changes height, and code that expects independent sides breaks. That is an LSP failure.
- I: small interfaces — The Interface Segregation Principle says no class should be forced to implement methods it does not use. Split a fat IWorker into IWorkable and IFeedable.
- D: depend on abstractions — The Dependency Inversion Principle says high-level code depends on an interface, not a concrete class. OrderService takes IOrderRepository, not SqlOrderRepository.
- DI is how DIP gets wired — Dependency injection (lesson 9) is the tool that hands the real class to the constructor. The principle is the idea. The container is the machinery.
- A tool, not a law — SOLID pays off in code that changes often. For a ten-line script, splitting everything into interfaces is noise, so use it where change hurts.
Quick check
Answer from memory — scrolling up first makes it stick less.
A subclass overrides a base method and throws NotSupportedException. Which principle is broken?
What does the Single Responsibility Principle really ask?
Code example
public record Customer(bool IsVip);
public record Order(Customer Customer, List<string> Items, decimal Total);
public interface IDiscountRule
{
bool AppliesTo(Order order);
decimal Discount(Order order);
}
public class VipDiscount : IDiscountRule
{
public bool AppliesTo(Order o) => o.Customer.IsVip;
public decimal Discount(Order o) => o.Total * 0.10m;
}
public class BulkDiscount : IDiscountRule
{
public bool AppliesTo(Order o) => o.Items.Count >= 10;
public decimal Discount(Order o) => 5m;
}
public class OrderService
{
private readonly IEnumerable<IDiscountRule> _rules;
public OrderService(IEnumerable<IDiscountRule> rules) => _rules = rules;
public decimal FinalTotal(Order order)
{
var discount = _rules
.Where(rule => rule.AppliesTo(order))
.Sum(rule => rule.Discount(order));
return order.Total - discount;
}
}Line-by-line walkthrough
- 1. IDiscountRule is a small contract: can I apply to this order, and how much do I take off.
- 2. VipDiscount and BulkDiscount each hold one rule. Each has one reason to change.
- 3. OrderService asks for IEnumerable of IDiscountRule in its constructor. It does not know which rules exist.
- 4. FinalTotal keeps only the rules that apply, sums their discounts, and subtracts the result.
- 5. A new seasonal discount is a new class added to the list. OrderService stays the same.
Spot the bug
public abstract class Bird
{
public abstract void Fly();
}
public class Penguin : Bird
{
public override void Fly()
{
throw new NotSupportedException("Penguins cannot fly.");
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Architectural principles (.NET) (Microsoft Learn)
- Interfaces in C# (Microsoft Learn)
Lock it in