CMD Guide
HomeOO & Low-Level Design

SOLID Principles

Step 2 in the OO & Low-Level Design path · 23 concepts · 0 problems

0 / 23 complete

📘 Learn SOLID Principles from zero

Imagine a busy restaurant kitchen. If one person took orders, cooked, washed dishes, and did payroll, every menu change or new health rule would disrupt that single overloaded person, and replacing them would collapse everything. Good kitchens split work into stations (grill, prep, dish) with clear handoffs, so changing one station rarely disturbs the others. This is Separation of Concerns: each station is highly cohesive (its tools all serve one job) and loosely coupled (stations talk through a simple ticket interface, not by reaching into each other's pans). SOLID — five principles collected by Robert C. Martin (the acronym coined by Michael Feathers) — applies the same idea to object-oriented code so change stays cheap and safe.

The five: Single Responsibility (one reason to change / one actor — the class-level form of "high cohesion, low coupling"), Open-Closed (open to extension, closed to modification), Liskov Substitution (a subtype must be usable anywhere its base type is, honoring the same contract: it may not strengthen preconditions or weaken postconditions), Interface Segregation (no client forced to depend on methods it doesn't use), Dependency Inversion (both layers depend on abstractions, not concretions).

Worked example. Suppose Invoice calculates totals and prints itself and saves to a database. A tax-law change (accounting), a UI redesign (design), and a database swap (ops) are three different actors all forcing edits to one file — risky and low-cohesion. Applying SRP, split into Invoice (totals), InvoicePrinter, and InvoiceRepository. Now apply DIP: Invoice talks to a Repository interface it owns, not MySQLRepository, so you can swap storage or inject a fake in tests — that's reduced coupling. The force across all five is the same: localize change — separate the things that vary independently and let them communicate through stable abstractions.

Key insight to remember: SOLID is not about more code; it is high cohesion plus loose coupling made concrete, so each axis of change is isolated and a future requirement touches one place, not many.

✨ Added by the guide to build intuition — not from the source course.

🎯 Guided practice

  1. Easy — spot the violation. A class UserService has methods register(user), sendWelcomeEmail(user), and renderProfileHtml(user). Which SOLID principle is broken, and why?

    Reasoning: Ask "how many actors would request a change to this class?" Registration logic answers to the business/auth owner; the email body answers to marketing; the HTML answers to the front-end team. That's three independent actors, so three reasons to change — a Single Responsibility violation, and concretely a cohesion problem (unrelated jobs glued together). Fix: keep UserService.register, extract EmailNotifier and a ProfileView (or template). Each unit is now cohesive and changes for exactly one reason, and you can test registration without touching email/UI.

  2. Medium — design with Open-Closed + DIP. A checkout computes shipping cost with if (type=="air") … else if (type=="ground") … else if (type=="drone") …. Each new carrier means editing this method. Redesign so adding a carrier requires no edits to existing code.

    Reasoning: The smell is a growing type-switch — the signal for Open-Closed. Step 1: define an abstraction, interface ShippingStrategy { Money cost(Package p); }. Step 2: make each carrier its own class implementing it: AirShipping, GroundShipping, DroneShipping — adding a carrier is now a new class (extension), not a modification of checkout (closed for modification). Step 3: apply Dependency InversionCheckout holds a ShippingStrategy injected via its constructor, so it depends on the interface it owns, never on a concrete carrier (loose coupling). Step 4 (guard with Liskov): every strategy must honor the same contract — accept any valid Package the interface promises (no strengthened precondition), and always return a non-negative Money without throwing for valid input (no weakened postcondition) — so Checkout can substitute any of them blindly. (Note: a real dispatcher still needs one place to select the strategy from input, e.g. a factory or registry; OCP confines that to a single isolated map, keeping the cost logic itself closed.) Result: new carriers plug in; the tested checkout code stays untouched.

✨ Added by the guide — work these before the full problem set.

Lessons in this topic

🧠 Review & recall

Active recall is what moves a topic into long-term memory. Flip each card before revealing, then test yourself — your results are saved on this device.

Flashcard
What do the five SOLID principles stand for, in order?
tap to reveal →
S = Single Responsibility (a class should have only one reason to change), O = Open/Closed (open for extension, closed for modification), L = Liskov Substitution (subclass objects must replace superclass objects without breaking correctness), I = Interface Segregation (clients shouldn't depend on interfaces they don't use), D = Dependency Inversion (depend on abstractions, not concrete implementations).
💡 SOLID = Sam's Old Lazy Iguana Dances; each letter is one OOP design rule.
Flashcard
How do cohesion and coupling each relate to the Single Responsibility Principle?
tap to reveal →
To follow SRP you want HIGH cohesion (a class's methods all serve one focused purpose) and LOW coupling (classes are independent, not tightly dependent on each other). Low cohesion or high coupling usually means a class is doing too many things, violating SRP.
💡 SRP = high cohesion + low coupling. 'Organized dustbins, not a garbage dump' (cohesion); 'universal remote, not a TV-only remote' (coupling).
Flashcard
In the lesson's Car/ElectricCar example, why does ElectricCar extending Car violate LSP, and how is it fixed?
tap to reveal →
ElectricCar can't honor Car's refuel() contract, so it throws UnsupportedOperationException, breaking any code that treats it as a Car. The fix is to break the hierarchy: introduce an abstract Vehicle base class with start(), stop(), and an abstract refuel(); ElectricCar's refuel() then internally calls recharge().
💡 If a subclass has to throw 'I can't do that', the hierarchy is wrong. Generalize up to Vehicle.
Flashcard
What are the telltale signs that code violates the Interface Segregation Principle?
tap to reveal →
Low cohesion (one interface mixing unrelated tasks), fat interfaces (too many methods covering unrelated functionality), and empty or unsupported methods (implementations that do nothing or throw UnsupportedOperationException because the class doesn't need that method). The fix is splitting the large interface into smaller, focused ones.
💡 ISP smell = a class throwing 'UnsupportedOperationException' for methods it was forced to implement (e.g. BasicPrinter.scanDocument).
Flashcard
State the Dependency Inversion Principle and how the lesson refactors a NotificationService to follow it.
tap to reveal →
DIP: high-level modules should not depend on low-level modules; both depend on abstractions, and abstractions shouldn't depend on details. The fix introduces a NotificationSender interface; EmailService and SMSService implement it, and NotificationService takes a NotificationSender via its constructor, so you can swap implementations without changing NotificationService.
💡 Both sides plug into the same interface socket; inject the dependency, don't 'new' it inside.
Flashcard
What does 'closed for modification' and 'open for extension' mean in the Open/Closed Principle, and how is OCP typically achieved?
tap to reveal →
Closed for modification: once a class is written and tested, you shouldn't change it to add new behavior (avoids reintroducing bugs into stable code). Open for extension: you can add new functionality via inheritance, interfaces, or polymorphism. Like a gaming console you extend with add-ons (steering wheel, VR headset) without rewiring its internals.
💡 Gaming console: add accessories (extend), never reopen the case (don't modify).
Q1. A UserManager class contains addUser(), deleteUser(), writeToFile(), and readFromFile(). Which SOLID principle is most directly violated, and why?
Q2. In the lesson, ElectricCar extends Car and overrides refuel() to throw UnsupportedOperationException. What is the core reason this breaks the Liskov Substitution Principle?
Q3. A single Printer interface declares printDocument(), scanDocument(), faxDocument(), and stapleDocument(). A BasicPrinter that can only print is forced to implement the other three by throwing exceptions. The best fix is to:
Q4. The lesson refactors NotificationService (which did `emailService = new EmailService()` internally) to follow DIP. Which change correctly achieves this?
Q5. Which pairing of SOLID concepts is correct?