I use SOLID principles in code reviews and interview preparation. They first seemed abstract to me. When I applied them, they helped me organize code so that changes affected fewer classes.
What Is SOLID?
SOLID is an acronym for five object oriented programming design principles introduced by Robert C. Martin. These principles help you write code that is easier to maintain, test, and extend. I find them useful in larger codebases.
Understanding OOP First
I first needed to understand object oriented programming (OOP). An object has:
- State: The data/attributes an object holds
- Behavior: The methods/actions an object can perform
A simple example I used to understand:
public class Person {
// State
private String name;
private int age;
// Constructor
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Behavior
public void sayHello() {
System.out.println("Hello, my name is " + name);
}
}
The following examples apply each SOLID principle to a common design problem.
Single Responsibility Principle (SRP)
One class, one reason to change.
I once wrote a class that handled user authentication, sent emails, and generated reports. A change to the email format required a change to that same class. Separate classes would isolate these responsibilities.
A class should have only one responsibility. A better approach is:
public class UserAuthenticator {
public boolean authenticate(String username, String password) {
// Only handles authentication
}
}
public class EmailService {
public void sendEmail(String to, String subject, String body) {
// Only handles email sending
}
}
public class ReportGenerator {
public void generateReport(Data data) {
// Only handles report generation
}
}
Open Closed Principle (OCP)
Open for extension, closed for modification.
I was working on a shape area calculator. My first version had if else statements for each shape type:
public class AreaCalculator {
public double calculateArea(Shape shape) {
if (shape instanceof Rectangle) {
// calculate rectangle area
} else if (shape instanceof Circle) {
// calculate circle area
}
// Adding a new shape means modifying this class!
}
}
Then I learned about polymorphism and abstract classes:
public abstract class Shape {
public abstract double calculateArea();
}
public class Rectangle extends Shape {
private double length;
private double width;
public double calculateArea() {
return length * width;
}
}
public class Circle extends Shape {
private double radius;
public double calculateArea() {
return Math.PI * radius * radius;
}
}
public class AreaCalculator {
public double calculateTotalArea(Shape[] shapes) {
double total = 0;
for (Shape shape : shapes) {
total += shape.calculateArea(); // Works for any Shape!
}
return total;
}
}
I can now add a shape class without a change to the existing shape classes.
Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types.
I made a mistake once. I created a Square class that extended Rectangle, but Square violated the rectangle's behavior:
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
}
public class Square extends Rectangle {
// This violates LSP!
public void setWidth(int width) {
this.width = width;
this.height = width; // Square forces width = height
}
public void setHeight(int height) {
this.width = height;
this.height = height; // This breaks rectangle behavior!
}
}
The problem: code expecting a Rectangle might break with a Square because Square changes the expected behavior.
Interface Segregation Principle (ISP)
Clients should not depend on interfaces they do not use.
I once created a huge interface that had methods for everything:
public interface BankAccount {
void deposit(double amount);
void withdraw(double amount);
double getBalance();
void transfer(BankAccount destination, double amount);
void calculateInterest();
}
But not all account types need all these methods. A savings account might not need transfer(), and a checking account might not need calculateInterest().
Better approach:
public interface DepositAccount {
void deposit(double amount);
void withdraw(double amount);
double getBalance();
}
public interface SavingsAccount extends DepositAccount {
void calculateInterest();
}
public interface TransferableAccount extends DepositAccount {
void transfer(BankAccount destination, double amount);
}
Dependency Inversion Principle (DIP)
Depend on abstractions, not concretions.
I was writing a notification service and initially hardcoded the email implementation:
public class NotificationService {
private EmailService emailService; // Depends on concrete class!
public void sendNotification(String message) {
emailService.sendEmail(message);
}
}
This made it hard to add SMS notifications later. I learned to depend on an interface instead:
public interface Notification {
void send(String message);
}
public class EmailNotification implements Notification {
public void send(String message) {
// Email implementation
}
}
public class SMSNotification implements Notification {
public void send(String message) {
// SMS implementation
}
}
public class NotificationService {
private Notification notification; // Depends on abstraction!
public NotificationService(Notification notification) {
this.notification = notification;
}
public void sendNotification(String message) {
notification.send(message); // Works with any Notification!
}
}
I can now replace an implementation or add another one without a change to NotificationService.
Putting It All Together
When I started applying SOLID principles, my code became:
- Easier to test: Each class has one responsibility
- Easier to extend: New features do not require changing existing code
- Easier to understand: Clear responsibilities and relationships
- More maintainable: Changes are isolated to specific classes
Principle summary
- SRP: One class, one responsibility
- OCP: Extend through inheritance/polymorphism, do not modify existing code
- LSP: Subtypes must be substitutable
- ISP and DIP: Keep interfaces focused and depend on abstractions, not concrete implementations
These principles helped me separate responsibilities and reduce the effect of changes. I use them to review the dependencies between classes.

Loading comments...