Introduction
Object-Oriented Programming is more than classes and inheritance. The principles that separate production-grade code from tutorial code are subtle but powerful. This article covers the five OOP principles that directly impact the scalability and maintainability of real-world Java systems.
1. Favor Composition Over Inheritance
Inheritance creates tight coupling. Composition creates flexibility.
Bad Code (Inheritance)
public class Vehicle {
public void start() { System.out.println("Starting vehicle"); }
}
public class ElectricCar extends Vehicle {
@Override
public void start() {
System.out.println("Starting electric motor silently");
}
}
public class HybridCar extends Vehicle {
// Now needs BOTH electric and petrol behavior — inheritance breaks down
}
Good Code (Composition)
public interface Engine {
void start();
}
public class ElectricEngine implements Engine {
public void start() { System.out.println("Electric motor humming"); }
}
public class PetrolEngine implements Engine {
public void start() { System.out.println("Petrol engine roaring"); }
}
public class Vehicle {
private final Engine engine;
public Vehicle(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
}
}
// Hybrid car? Just compose both engines.
Vehicle electric = new Vehicle(new ElectricEngine());
Vehicle petrol = new Vehicle(new PetrolEngine());
2. Program to Interfaces, Not Implementations
Bad Code
public class OrderService {
private ArrayList<Order> orders = new ArrayList<>();
private HashMap<String, Order> orderIndex = new HashMap<>();
}
Good Code
public class OrderService {
private List<Order> orders = new ArrayList<>();
private Map<String, Order> orderIndex = new HashMap<>();
}
Why does this matter? Because you can swap ArrayList for LinkedList or HashMap for TreeMap without changing any calling code. In large systems, this is the difference between a 1-line change and a 100-file refactor.
Real-World Example
// Bad — tightly coupled to MySQL
public class ReportService {
private MySQLConnection connection;
}
// Good — works with any database
public class ReportService {
private DataSource dataSource; // Interface from javax.sql
}
3. Encapsulation Done Right
Encapsulation is not just about making fields private. It is about controlling how state changes.
Bad Code
public class BankAccount {
public double balance; // Anyone can set this to -1000
}
// Usage
account.balance = account.balance - amount; // No validation!
Good Code
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
if (initialBalance < 0) throw new IllegalArgumentException("Initial balance cannot be negative");
this.balance = initialBalance;
}
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
if (amount > balance) throw new InsufficientFundsException();
this.balance -= amount;
}
public double getBalance() {
return balance; // Read-only access
}
}
Immutable Objects
The strongest form of encapsulation. Once created, state cannot change.
public record Money(double amount, String currency) {
public Money {
if (amount < 0) throw new IllegalArgumentException("Negative amount");
if (currency == null || currency.isBlank()) throw new IllegalArgumentException("Currency required");
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) throw new IllegalArgumentException("Currency mismatch");
return new Money(this.amount + other.amount, this.currency);
}
}
Immutable objects are inherently thread-safe and eliminate an entire category of bugs.
4. High Cohesion & Low Coupling
Cohesion: How closely related are the responsibilities within a single module? Coupling: How dependent is one module on another?
Bad Code (Low Cohesion)
public class UserManager {
public void createUser(User user) { ... }
public void sendWelcomeEmail(User user) { ... }
public void generateInvoice(User user) { ... }
public void backupDatabase() { ... }
}
Good Code (High Cohesion)
public class UserService {
public void createUser(User user) { ... }
public User findById(String id) { ... }
public void updateProfile(String id, ProfileUpdate update) { ... }
}
public class EmailService {
public void sendWelcomeEmail(User user) { ... }
}
public class InvoiceService {
public void generateInvoice(User user) { ... }
}
Each service has a clear, focused purpose. They communicate through well-defined interfaces, not by sharing internal state.
5. Tell, Don't Ask
Instead of asking an object for its data and making decisions externally, tell the object what to do.
Bad Code (Ask)
// Caller makes the decision
if (account.getBalance() >= amount) {
account.setBalance(account.getBalance() - amount);
} else {
throw new InsufficientFundsException();
}
Good Code (Tell)
// Object makes the decision
account.withdraw(amount); // Encapsulates the validation internally
6. Law of Demeter (Don't Talk to Strangers)
A method should only call methods on:
this- Its parameters
- Objects it creates
- Its direct fields
Bad Code
// Reaching deep into object graph
String city = order.getCustomer().getAddress().getCity();
double rate = order.getCustomer().getMembership().getDiscount().getRate();
Good Code
// Order provides what you need
String city = order.getShippingCity();
double rate = order.getCustomerDiscountRate();
Deep chaining creates fragile dependencies. If Address changes its structure, every caller breaks.
Interview Questions
-
When is inheritance appropriate? When there is a true "is-a" relationship (e.g.,
Dog extends Animal) AND the subclass does not override base behavior in surprising ways. In practice, prefer composition. -
What is the difference between cohesion and coupling? Cohesion is internal (how focused a class is). Coupling is external (how dependent classes are on each other). Aim for high cohesion and low coupling.
-
Why are immutable objects preferred in concurrent systems? They cannot be modified after creation, eliminating race conditions and the need for synchronization.
Summary
| Principle | Rule |
|---|---|
| Composition over Inheritance | Use "has-a" instead of "is-a" |
| Program to Interfaces | Declare types as interfaces, not concrete classes |
| Encapsulation | Control state changes, use immutable objects |
| High Cohesion / Low Coupling | Focused classes, minimal dependencies |
| Tell Don't Ask | Let objects manage their own behavior |
| Law of Demeter | Don't chain through object graphs |