How do the SOLID principles show up in ordinary Java code, with a worked example?
SOLID is five principles, and the useful way to answer this question is to show where each one lives in a piece of code rather than reciting the definitions.
Single responsibility means one reason to change. An Invoice should calculate an invoice; it should not also format it, persist it or send it.
Open/closed means open for extension, closed for modification. When you add a new tax rule, the invoice code should not change — you add a new implementation of the tax policy.
Liskov substitution means a subtype must be usable wherever its parent is. If a subclass changes the contract — by throwing where the parent did not, or by weakening a postcondition — the principle is broken.
Interface segregation means small, focused interfaces. A class should not be forced to implement methods it has no use for.
Dependency inversion means depending on abstractions, not on concrete classes. That is what the tax policy interface is: the invoice depends on the interface, and the specific rule is injected.
import java.util.List;
public class Main {
public static void main(String[] args) {
// SRP: one reason to change per class.
Invoice invoice = new Invoice(List.of(1000, 2500));
// OCP: add a new tax rule without touching the invoice.
TaxPolicy policy = new VatPolicy(20);
// DIP: the formatter can be swapped.
PriceFormatter formatter = new PriceFormatter();
System.out.println("subtotal : " + formatter.format(invoice.subtotal()));
System.out.println("total : " + formatter.format(invoice.total(policy)));
}
}
class Invoice {
private final List<Integer> lines;
Invoice(List<Integer> lines) {
this.lines = List.copyOf(lines);
}
int subtotal() {
int total = 0;
for (int line : lines) {
total += line;
}
return total;
}
int total(TaxPolicy policy) {
return subtotal() + policy.taxOn(subtotal());
}
}
interface TaxPolicy {
int taxOn(int amount);
}
class VatPolicy implements TaxPolicy {
private final int percent;
VatPolicy(int percent) {
this.percent = percent;
}
@Override
public int taxOn(int amount) {
return amount * percent / 100;
}
}
class PriceFormatter {
String format(int pence) {
return String.format("£%.2f", pence / 100.0);
}
}The output shows the invoice calculating a subtotal of 35 pounds, and a total of 42 pounds once the value added tax is applied. The invoice never mentions the tax rate — it only knows the interface.
Two of the principles are hard to demonstrate in a snippet. Liskov substitution is about what happens when you pass a subtype where the parent is expected, so the example that proves it is one where a subclass would break the contract. Interface segregation is about not fattening an interface with methods an implementor does not need. But all five show up as ordinary decisions about where to put a method, which interface to type a field against, and what to inject.