Mastering the Six Core Principles of Software Design
1. Single Responsibility Principle
A class or interface should have only one reason to change. This means each component must handle a single, well-defined concern.
Violation Example: Consider a phone system with four operations: dialing, chatting, responding, and hanging up. Combining all into a single interface leads to tight coupling:
public interface IPhone {
void dial();
void chat();
void hangup();
}
Refactored Solution: Split responsibilities into distinct interfaces:
public interface ConnectionHandler {
void initiateCall();
void terminateCall();
}
public interface DataChannel {
void transmitData(ConnectionHandler handler);
}
public class Phone implements ConnectionHandler, DataChannel {
// Implementation details
}
This separation ensures that changes in one area don't affect unrelated functionality.
2. Liskov Substitution Principle
Subtypes must be substitutable for their base types without altering expected behavior. Key rules:
- Subclasses must fully implement inherited methods; partial implementation breaks the contract.
- Subclasses may introduce new behaviors not present in the parent.
- Parameter types in overridden methods can be broader (e.g., using Object instead of String).
- Return types in overridden methods must be narrower or identical to those in the parent.
Violating these rules risks runtime errors when substituting derived types.
3. Dependency Inversion Principle
High-level modules should not depend on low-level ones. Instead, both should depend on abstractions. The core tenets are:
- Dependencies are based on interfaces or abstract classes, not concrete implementations.
- Abstractions should not rely on details.
- Details should depend on abstractions.
This enables flexible, testable, and maintainable systems.
Dependency Injection Methods:
- Constructor Injection:
public class Driver {
private final Vehicle vehicle;
public Driver(Vehicle vehicle) {
this.vehicle = vehicle;
}
}
- Setter Injection:
public class Driver {
private Vehicle vehicle;
public void setVehicle(Vehicle vehicle) {
this.vehicle = vehicle;
}
}
- Method Parameter Injection:
public class Driver {
public void operate(Vehicle vehicle) {
vehicle.start();
}
}
Best practices:
- Every class should have an interface or abstract base.
- Use abstraction as variable type.
- Avoid deriving from concrete classes.
- Minimize overriding implemented methods.
- Combine with Liskov Substitution for robust design.
4. Interface Segregation Principle
Clients should not be forced to depend on methods they do not use. Design interfaces that are small, focused, and minimal.
Key points:
- Avoid large, monolithic interfaces.
- Split broad interfaces into smaller, cohesive ones.
- Each interface should serve a specific client need.
Distinction from SRP:
- SRP focuses on logical responsibility within a class or interface.
- ISP emphasizes minimizing exposure—only expose what is necessary.
Always ensure SRP is met before applying ISP. A well-designed interface has minimal public methods and high cohesion.
5. Law of Demeter (Least Knowledge Principle)
Each object should know as little as possible about other objects. Only interact with direct dependencies—your 'friends'.
Friend Classes:
- Appear as member variables.
- Used as method parameters or return types.
The principle discourages deep object chains like a.getB().getC().doSomething(). Instead, prefer:
public class Service {
public void execute(Processor processor) {
processor.process();
}
}
This reduces coupling and increases modularity. Combined with Interface Segregation, it promotes high cohesion and low coupling.
6. Open-Closed Principle
Software entities should be open for extension but closed for modification. New behavior is added via inheritance or composition, not by changing existing code.
Example: Boookstore Pricing Logic
Define a clean interface:
public interface Book {
String getTitle();
int getPriceInCents();
String getAuthor();
}
public class FictionBook implements Book {
private final String title;
private final int priceInCents;
private final String author;
public FictionBook(String title, int priceInCents, String author) {
this.title = title;
this.priceInCents = priceInCents;
this.author = author;
}
@Override
public String getTitle() { return title; }
@Override
public int getPriceInCents() { return priceInCents; }
@Override
public String getAuthor() { return author; }
}
For a 90% discount on books over 4000 cents:
public class DiscountedFictionBook implements Book {
private final Book original;
public DiscountedFictionBook(Book book) {
this.original = book;
}
@Override
public String getTitle() {
return original.getTitle();
}
@Override
public int getPriceInCents() {
int basePrice = original.getPriceInCents();
return basePrice > 4000 ? (int)(basePrice * 0.9) : basePrice;
}
@Override
public String getAuthor() {
return original.getAuthor();
}
}
The bookstore logic remains unchanged—just swap in the discounted variant. No modifications to existing classes required.