SOLID Principles with Runnable Examples
The five SOLID principles with short Java examples, including a Liskov violation that gave area 4 where a rectangle contract expected 10, and when applying them goes too far.
SOLID is five object-oriented design guidelines popularised by Robert C. Martin: single responsibility, open/closed, Liskov substitution, interface segregation and dependency inversion, all aimed at code that is easy to change. The example interviewers expect is the Liskov violation: a mutable Square subclass of Rectangle breaks callers that expect width and height to be independent.
SOLID bundles five design guidelines: single responsibility, open/closed, Liskov substitution, interface segregation and dependency inversion. They aim at code that is easy to change. A Square subclass of a mutable Rectangle broke the contract: setting width 5 and height 2 gave area 4, not 10. Treat them as heuristics, not laws.
- SOLID stands for single responsibility, open/closed, Liskov substitution, interface segregation and dependency inversion, five design principles popularised by Robert C. Martin.
- The single responsibility principle says a class should have one reason to change, tied to who asks for the change, not to the number of methods.
- A mutable Square subclass of Rectangle violates Liskov substitution: setting width 5 and height 2 gave area 4, where a Rectangle caller expects 10.
- The interface segregation principle favours small role-based interfaces, such as Printer, Scanner and Fax, over one broad Machine interface that forces stub methods.
- Dependency inversion says high-level and low-level modules should both depend on abstractions; dependency injection is one technique for supplying them.
01What SOLID is for
SOLID is five principles popularised by Robert C. Martin. They are not rules a compiler checks. They are guidelines that help you notice designs that will be painful to change: classes with too many reasons to change, edits that ripple through unrelated code, subclasses that surprise their callers.
Use them as questions to ask in code review, not as a checklist to satisfy. Applying them mechanically produces layers of tiny classes and interfaces that nobody needs.
02S: Single responsibility
If an Invoice class calculates totals, formats a PDF and saves to the database, then a change in tax rules, a change in layout and a change in the schema all edit the same class. Split them: Invoice holds data and the calculation, InvoicePdfWriter renders, InvoiceRepository stores. Each now changes for one kind of reason, and each can be tested alone.
"Responsibility" means a reason to change, tied to who asks for the change, not the number of methods. A class with ten methods that all serve one purpose is fine. Splitting too early produces dozens of anemic classes, so wait until you feel real friction.
03O: Open/closed
Add new behaviour by adding code, not by editing tested code. The classic fix is replacing a growing switch with polymorphism. The factory and strategy topics show this: a registry of creators or a map of discount strategies lets you add "square" or a new discount with one line and no change to the code that uses them. A new discount became a new lambda in a map, and the calculation loop did not change.
The principle only pays off where change is likely. Do not build extension points for variations nobody has asked for. When the first change arrives, refactor to make the second one cheap.
04L: Liskov substitution
If code works with a Rectangle, it should work with any subclass of Rectangle. Here is a famous violation:
static class Rectangle {
int w, h;
void setW(int x) { w = x; }
void setH(int x) { h = x; }
int area() { return w * h; }
}
static class Square extends Rectangle {
@Override void setW(int x) { w = h = x; }
@Override void setH(int x) { w = h = x; }
}
Rectangle r = new Square();
r.setW(5); r.setH(2);
System.out.println(r.area()); // 4, but a Rectangle caller expects 10The program printed 4. A caller holding a Rectangle reasonably expects width 5 times height 2 to give 10. The Square overrides changed the contract: setting one side secretly changes the other. In mathematics a square is a rectangle, but with mutable setters it is not substitutable. Fixes: make shapes immutable (a square then is a valid rectangle value), or do not model Square as a subclass.
Liskov also forbids a subclass from strengthening preconditions, weakening postconditions, or throwing exceptions the parent does not. For example, a ReadOnlyBag that extends a mutable Bag class and throws UnsupportedOperationException on add breaks code written against Bag. (The JDK's List.add is documented as optional, so unmodifiable lists are within that contract.)
05I: Interface segregation
A large interface such as Machine { print(); scan(); fax(); } forces a basic printer to implement scan and fax, usually by throwing "not supported". Split it into Printer, Scanner and Fax, and let a multifunction device implement all three. Clients then depend only on what they use, and a change to the fax interface does not affect code that only prints.
In practice: prefer several small, role-based interfaces to one broad one. Java's Comparable, Runnable and Closeable are small by design.
06D: Dependency inversion
High-level policy (what the business does) should not depend on low-level detail (how it is stored or sent). Both should depend on an interface. If OrderService creates a MySqlOrderRepository with new, it is welded to MySQL and hard to test. If it takes an OrderRepository interface in its constructor, any implementation can be supplied, including a fake in tests. The topic on dependency injection shows this in code.
Note the word "inversion": the interface is owned by the high-level code, and the low-level code conforms to it, reversing the usual direction of dependency.
07When SOLID is overdone
Interfaces with a single implementation, and classes split into fragments so small that you must open six files to follow one action, make code harder to read without making it easier to change. Duplication is sometimes cheaper than the wrong abstraction. Apply a principle when you can name the concrete change it makes easier, not because it is on the list.
A good habit: write the straightforward version, and let the second or third similar case reveal what should be abstracted.
08Common interview questions
Q: What does SRP mean?
A: A class should have one reason to change, meaning one stakeholder or concern it serves.
Q: What is the open/closed principle?
A: Add new behaviour by extending (new classes or entries), not by modifying existing, tested code.
Q: Give an LSP violation.
A: A mutable Square extending Rectangle, where setting the width also changes the height, breaks code that expects independent dimensions.
Q: What is interface segregation?
A: Prefer small, focused interfaces so clients do not depend on methods they do not use.
Q: What does dependency inversion say?
A: High-level modules and low-level modules should both depend on abstractions, and abstractions should not depend on details.
Q: Dependency inversion versus dependency injection?
A: Inversion is the design principle. Injection is a technique for supplying the dependencies from outside.