SOLID Principles Skill
Review and apply SOLID principles in Java code.
When to Use
- User says "check SOLID" / "SOLID review" / "is this class doing too much?"
- Reviewing class design
- Refactoring large classes
- Code review focusing on design
Quick Reference
| Letter | Principle | One-liner |
|---|---|---|
| S | Single Responsibility | One class = one reason to change |
| O | Open/Closed | Open for extension, closed for modification |
| L | Liskov Substitution | Subtypes must be substitutable for base types |
| I | Interface Segregation | Many specific interfaces > one general interface |
| D | Dependency Inversion | Depend on abstractions, not concretions |
S - Single Responsibility Principle (SRP)
"A class should have only one reason to change."
Violation
// ❌ BAD: UserService does too much
public class UserService {
public User createUser(String name, String email) {
// validation logic
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
// persistence logic
User user = new User(name, email);
entityManager.persist(user);
// notification logic
String subject = "Welcome!";
String body = "Hello " + name;
emailClient.send(email, subject, body);
// audit logic
auditLog.log("User created: " + email);
return user;
}
}
Problems:
- Validation changes? Modify UserService
- Email template changes? Modify UserService
- Audit format changes? Modify UserService
- Hard to test each concern separately
Refactored
// ✅ GOOD: Each class has one responsibility
public class UserValidator {
public void validate(String name, String email) {
if (email == null || !email.contains("@")) {
throw new ValidationException("Invalid email");
}
}
}
public class UserRepository {
public User save(User user) {
entityManager.persist(user);
return user;
}
}
public class WelcomeEmailSender {
public void sendWelcome(User user) {
String subject = "Welcome!";
String body = "Hello " + user.getName();
emailClient.send(user.getEmail(), subject, body);
}
}
public class UserAuditLogger {
public void logCreation(User user) {
auditLog.log("User created: " + user.getEmail());
}
}
public class UserService {
private final UserValidator validator;
private final UserRepository repository;
private final WelcomeEmailSender emailSender;
private final UserAuditLogger auditLogger;
public User createUser(String name, String email) {
validator.validate(name, email);
User user = repository.save(new User(name, email));
emailSender.sendWelcome(user);
auditLogger.logCreation(user);
return user;
}
}
How to Detect SRP Violations
- Class has many
importstatements from different domains - Class name contains "And" or "Manager" or "Handler" (often)
- Methods operate on unrelated data
- Changes in one area require touching unrelated methods
- Hard to name the class concisely
Quick Check Questions
- Can you describe the class purpose in one sentence without "and"?
- Would different stakeholders request changes to this class?
- Are there methods that don't use most of the class fields?
O - Open/Closed Principle (OCP)
"Software entities should be open for extension, but closed for modification."
Violation
// ❌ BAD: Must modify class to add new discount type
public class DiscountCalculator {
public double calculate(Order order, String discountType) {
if (discountType.equals("PERCENTAGE")) {
return order.getTotal() * 0.1;
} else if (discountType.equals("FIXED")) {
return 50.0;
} else if (discountType.equals("LOYALTY")) {
return order.getTotal() * order.getCustomer().getLoyaltyRate();
}
// Every new discount type = modify this class
return 0;
}
}
Refactored
// ✅ GOOD: Add new discounts without modifying existing code
public interface DiscountStrategy {
double calculate(Order order);
boolean supports(String discountType);
}
public class PercentageDiscount implements DiscountStrategy {
@Override
public double calculate(Order order) {
return order.getTotal() * 0.1;
}
@Override
public boolean supports(String discountType) {
return "PERCENTAGE".equals(discountType);
}
}
public class FixedDiscount implements DiscountStrategy {
@Override
public double calculate(Order order) {
return 50.0;
}
@Override
public boolean supports(String discountType) {
return "FIXED".equals(discountType);
}
}
public class LoyaltyDiscount implements DiscountStrategy {
@Override
public double calculate(Order order) {
return order.getTotal() * order.getCustomer().getLoyaltyRate();
}
@Override
public boolean supports(String discountType) {
return "LOYALTY".equals(discountType);
}
}
// New discount? Just add new class, no modification needed
public class SeasonalDiscount implements DiscountStrategy {
@Override
public double calculate(Order order) {
return order.getTotal() * 0.2;
}
@Override
public boolean supports(String discountType) {
return "SEASONAL".equals(discountType);
}
}
public class DiscountCalculator {
private final List<DiscountStrategy> strategies;
public DiscountCalculator(List<DiscountStrategy> strategies) {
this.strategies = strategies;
}
public double calculate(Order order, String discountType) {
return strategies.stream()
.filter(s -> s.supports(discountType))
.findFirst()
.map(s -> s.calculate(order))
.orElse(0.0);
}
}
How to Detect OCP Violations
if/elseorswitchon type/status that grows over time- Enum-based dispatching with frequent new values
- Changes require modifying core classes
Common OCP Patterns
| Pattern | Use When |
|---|---|
| Strategy | Multiple algorithms for same operation |
| Template Method | Same structure, different steps |
| Decorator | Add behavior dynamically |
| Factory | Create objects without specifying class |
L - Liskov Substitution Principle (LSP)
"Subtypes must be substitutable for their base types."
Violation
// ❌ BAD: Square violates Rectangle contract
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 int getArea() {
return width * height;
}
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Violates expected behavior!
}
@Override
public void setHeight(int height) {
this.width = height; // Violates expected behavior!
this.height = height;
}
}
// This test fails for Square!
void testRectangle(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert r.getArea() == 20; // Square returns 16!
}
Refactored
// ✅ GOOD: Separate abstractions
public interface Shape {
int getArea();
}
public class Rectangle implements Shape {
private final int width;
private final int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
public class Square implements Shape {
private final int side;
public Square(int side) {
this.side = side;
}
@Override