SOLID Principles in Java
Code that works today must also be easy to change tomorrow, when the business adds a new discount, a new payment method or a new way to notify customers. SOLID is a set of five principles, collected by Robert C. Martin, that keep object-oriented code flexible. Each one names a common way designs go wrong, and the fix. This lesson explains all five, then applies them to the OrderManager "god class" from Appendix G2.
The Five Principles
| Letter | Principle | In one line | Smell it fixes |
|---|---|---|---|
| S | Single responsibility | A class should have one reason to change | A class that saves, emails and prints |
| O | Open-closed | Open for extension, closed for modification | An if-else chain edited for every new case |
| L | Liskov substitution | A subclass must be usable anywhere its parent is | Overrides that throw "not supported" or change the meaning |
| I | Interface segregation | Clients shouldn't depend on methods they don't use | One fat interface that every class half-implements |
| D | Dependency inversion | Depend on abstractions, not concrete classes | new MySqlDatabase() inside business logic |
Each Principle in Practice
S: single responsibility. Appendix G2's OrderManager validates items, calculates discounts, writes SQL, sends email and builds invoices. A change to the email text, the database or the tax rules all touch the same class. Split it: OrderService, OrderRepository, Notifier, InvoicePrinter, each with one job.
O: open-closed. Discounts there are an if-else on "GOLD", "SILVER", "STAFF". Adding "PLATINUM" means editing tested code. Put each discount behind a DiscountPolicy interface; a new discount is a new class, and nothing old changes (the Strategy pattern).
L: Liskov substitution. The classic broken example is Square extends Rectangle. Code that sets a rectangle's width to 5 and height to 4 expects an area of 20; a Square that keeps its sides equal gives 16. If a subclass can't honor the parent's promises, it shouldn't extend it.
I: interface segregation. A Machine interface with print(), scan() and fax() forces a simple printer to "implement" scanning by throwing an exception. Split it into Printer, Scanner and Fax; a multifunction device implements all three.
D: dependency inversion. OrderManager calls DriverManager.getConnection(...) directly, so it can't run without MySQL, even in a test. Make it depend on an OrderRepository interface, and pass a real or in-memory implementation in through the constructor.
The God Class, Refactored
Here's the heart of the G2 refactor, with a fake repository and notifier so it runs anywhere:
import java.util.ArrayList;
import java.util.List;
public class Solid {
record Item(String name, long price, int qty) {
long total() { return price * qty; }
}
interface DiscountPolicy { long apply(long amount); }
interface OrderRepository { void save(String email, long total); }
interface Notifier { void send(String email, String text); }
static class GoldDiscount implements DiscountPolicy {
public long apply(long amount) { return amount * 80 / 100; }
}
static class NoDiscount implements DiscountPolicy {
public long apply(long amount) { return amount; }
}
static class InMemoryRepository implements OrderRepository {
final List<String> rows = new ArrayList<>();
public void save(String email, long total) {
rows.add(email + "=" + total);
}
}
static class ConsoleNotifier implements Notifier {
public void send(String email, String text) {
System.out.println("To " + email + ": " + text);
}
}
static class OrderService {
private final OrderRepository repository;
private final Notifier notifier;
OrderService(OrderRepository repository, Notifier notifier) {
this.repository = repository;
this.notifier = notifier;
}
long place(String email, List<Item> items, DiscountPolicy policy) {
long subtotal = 0;
for (Item i : items) {
subtotal += i.total();
}
long total = policy.apply(subtotal);
repository.save(email, total);
notifier.send(email, "your order total is Rs. " + total);
return total;
}
}
public static void main(String[] args) {
InMemoryRepository repo = new InMemoryRepository();
OrderService service = new OrderService(repo, new ConsoleNotifier());
List<Item> cart = List.of(
new Item("Headphones", 2500, 1), new Item("Cable", 300, 2));
service.place("navika@example.com", cart, new GoldDiscount());
service.place("srihitha@example.com", cart, new NoDiscount());
System.out.println("Saved: " + repo.rows);
}
}To navika@example.com: your order total is Rs. 2480
To srihitha@example.com: your order total is Rs. 3100
Saved: [navika@example.com=2480, srihitha@example.com=3100]How it works
- S:
OrderServiceonly coordinates placing an order. Saving, notifying and discounting each live in their own class. - O: a new discount is a new
DiscountPolicyclass;OrderServicenever changes. - D:
OrderServicedepends on theOrderRepositoryandNotifierinterfaces, received through its constructor.mainplugs in in-memory versions; production code would plug in a JDBC repository and an email sender. - I: each interface has a single method; no class implements anything it doesn't need.
- Money is a
longof rupees and the item is a record, which removes G2'sString[]anddoubleproblems too.
Common Mistakes
Splitting classes into tiny pieces with no real reason. One-line classes everywhere are as hard to follow as a god class.
Subclassing for code reuse when the "is-a" promise doesn't hold. That's the Liskov trap; use composition.
Creating dependencies with new inside business code. It welds the class to one implementation.
Interview Corner
What are the SOLID principles?
Single responsibility: a class should have one reason to change. Open-closed: classes should be open for extension but closed for modification, so new behavior comes from new code. Liskov substitution: subtypes must be usable wherever their base type is expected without breaking correctness. Interface segregation: prefer small, client-specific interfaces over large general ones. Dependency inversion: high-level modules should depend on abstractions, not on low-level concrete classes.
Give an example of violating the Liskov substitution principle.
Square extends Rectangle: if a square overrides setWidth to also change the height, code written for rectangles that sets width 5 and height 4 expects an area of 20 but gets 16. The subclass can't be substituted for its parent without surprising callers, so it shouldn't extend it.
Practice
- Easy: Add a
StaffDiscount(50% off) toSolid.javaand place an order with it. Which existing classes did you edit? - Harder: Complete the G2 refactor: add an
InvoicePrinterclass that prints each item and the total, and call it fromOrderServicethrough an interface.
Key Takeaways
Was this tutorial helpful?