Knowledge Guide
HomeOO & Low-Level Design

SOLID Principles

Step 3 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