The SOLID principles are five fundamental design principles that help developers create maintainable, flexible, and scalable object-oriented software. These principles, introduced by Robert C. Martin (Uncle Bob), form the foundation of clean architecture and good software design.
What are SOLID Principles?
SOLID is an acronym that stands for:
- S - Single Responsibility Principle (SRP)
- O - Open/Closed Principle (OCP)
- L - Liskov Substitution Principle (LSP)
- I - Interface Segregation Principle (ISP)
- D - Dependency Inversion Principle (DIP)
Let's explore each principle with detailed Java examples showing both violations and correct implementations.
1. Single Responsibility Principle (SRP)
Definition
A class should have only one reason to change, meaning it should have only one job or responsibility.
❌ Violating SRP
// BAD: This class has multiple responsibilities
public class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
// Responsibility 1: User data management
public String getName() { return name; }
public String getEmail() { return email; }
// Responsibility 2: Email validation
public boolean isValidEmail() {
return email.contains("@") && email.contains(".");
}
// Responsibility 3: Database operations
public void saveToDatabase() {
// Database connection and save logic
System.out.println("Saving user to database...");
}
// Responsibility 4: Email sending
public void sendWelcomeEmail() {
// Email sending logic
System.out.println("Sending welcome email to " + email);
}
// Responsibility 5: Data formatting
public String formatUserInfo() {
return "User: " + name + " (" + email + ")";
}
}
✅ Following SRP
// GOOD: Each class has a single responsibility
// Responsibility 1: User data management
public class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() { return name; }
public String getEmail() { return email; }
}
// Responsibility 2: Email validation
public class EmailValidator {
public boolean isValid(String email) {
return email != null && email.contains("@") && email.contains(".");
}
}
// Responsibility 3: Database operations
public class UserRepository {
public void save(User user) {
System.out.println("Saving user to database: " + user.getName());
}
}
// Responsibility 4: Email service
public class EmailService {
public void sendWelcomeEmail(User user) {
System.out.println("Sending welcome email to " + user.getEmail());
}
}
// Responsibility 5: Data formatting
public class UserFormatter {
public String format(User user) {
return "User: " + user.getName() + " (" + user.getEmail() + ")";
}
}
Advantages of SRP
- Easier maintenance: Changes to one responsibility don't affect others
- Better testability: Each class can be tested independently
- Improved readability: Classes are focused and easier to understand
- Reduced coupling: Classes depend on fewer things
2. Open/Closed Principle (OCP)
Definition
Software entities should be open for extension but closed for modification.
❌ Violating OCP
// BAD: Adding new shapes requires modifying existing code
public class AreaCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
return rectangle.getWidth() * rectangle.getHeight();
} else if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return Math.PI * circle.getRadius() * circle.getRadius();
}
// Adding a new shape requires modifying this method
return 0;
}
}
class Rectangle {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getWidth() { return width; }
public double getHeight() { return height; }
}
class Circle {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getRadius() { return radius; }
}
✅ Following OCP
// GOOD: Using abstraction to allow extension without modification
// Abstract base class
abstract class Shape {
public abstract double calculateArea();
}
// Concrete implementations
class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
// New shapes can be added without modifying existing code
class Triangle extends Shape {
private double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double calculateArea() {
return 0.5 * base * height;
}
}
// Calculator doesn't need to be modified for new shapes
public class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.calculateArea();
}
public double calculateTotalArea(Shape[] shapes) {
double total = 0;
for (Shape shape : shapes) {
total += shape.calculateArea();
}
return total;
}
}
Advantages of OCP
- Extensibility: Easy to add new functionality without breaking existing code
- Maintainability: Reduces risk of introducing bugs in working code
- Flexibility: System can be extended with minimal effort
- Code reusability: Existing code can be reused with new extensions
3. Liskov Substitution Principle (LSP)
Definition
Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.
❌ Violating LSP
// BAD: Penguin violates LSP because it can't fly
class Bird {
public void fly() {
System.out.println("Bird is flying");
}
public void eat() {
System.out.println("Bird is eating");
}
}
class Sparrow extends Bird {
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
class Penguin extends Bird {
@Override
public void fly() {
// Penguins can't fly! This violates LSP
throw new UnsupportedOperationException("Penguins cannot fly");
}
}
// This code will break when Penguin is used
public class BirdTest {
public static void makeBirdFly(Bird bird) {
bird.fly(); // Will throw exception for Penguin
}
public static void main(String[] args) {
Bird sparrow = new Sparrow();
Bird penguin = new Penguin();
makeBirdFly(sparrow); // Works fine
makeBirdFly(penguin); // Throws exception - LSP violation
}
}
✅ Following LSP
// GOOD: Proper hierarchy that respects LSP
// Base class with common behavior
abstract class Bird {
public abstract void eat();
public abstract void makeSound();
}
// Interface for flying capability
interface Flyable {
void fly();
}
// Interface for swimming capability
interface Swimmable {
void swim();
}
// Flying birds
class Sparrow extends Bird implements Flyable {
@Override
public void eat() {
System.out.println("Sparrow is eating seeds");
}
@Override
public void makeSound() {
System.out.println("Sparrow chirps");
}
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
class Eagle extends Bird implements Flyable {
@Override
public void eat() {
System.out.println("Eagle is hunting");
}
@Override
public void makeSound() {
System.out.println("Eagle screeches");
}
@Override
public void fly() {
System.out.println("Eagle soars high");
}
}
// Swimming birds
class Penguin extends Bird implements Swimmable {
@Override
public void eat() {
System.out.println("Penguin is eating fish");
}
@Override
public void makeSound() {
System.out.println("Penguin trumpets");
}
@Override
public void swim() {
System.out.println("Penguin is swimming");
}
}
// Now substitution works correctly
public class BirdTest {
public static void feedBird(Bird bird) {
bird.eat(); // Works for all birds
bird.makeSound(); // Works for all birds
}
public static void makeFlyableFly(Flyable flyable) {
flyable.fly(); // Only called on flying birds
}
public static void makeSwimmableSwim(Swimmable swimmable) {
swimmable.swim(); // Only called on swimming birds
}
}
Advantages of LSP
- Reliability: Substitution doesn't break existing functionality
- Polymorphism: True polymorphic behavior without unexpected exceptions
- Design integrity: Maintains logical consistency in inheritance hierarchies
- Code robustness: Prevents runtime errors due to violated contracts
4. Interface Segregation Principle (ISP)
Definition
A client should not be forced to implement interfaces it doesn't use.
❌ Violating ISP
// BAD: Fat interface that forces unnecessary implementations
interface MultiFunctionDevice {
void print(String document);
void scan(String document);
void fax(String document);
void copy(String document);
void email(String document);
}
// Simple printer forced to implement methods it doesn't need
class SimplePrinter implements MultiFunctionDevice {
@Override
public void print(String document) {
System.out.println("Printing: " + document);
}
@Override
public void scan(String document) {
throw new UnsupportedOperationException("Simple printer cannot scan");
}
@Override
public void fax(String document) {
throw new UnsupportedOperationException("Simple printer cannot fax");
}
@Override
public void copy(String document) {
throw new UnsupportedOperationException("Simple printer cannot copy");
}
@Override
public void email(String document) {
throw new UnsupportedOperationException("Simple printer cannot email");
}
}
// Scanner forced to implement methods it doesn't need
class Scanner implements MultiFunctionDevice {
@Override
public void print(String document) {
throw new UnsupportedOperationException("Scanner cannot print");
}
@Override
public void scan(String document) {
System.out.println("Scanning: " + document);
}
@Override
public void fax(String document) {
throw new UnsupportedOperationException("Scanner cannot fax");
}
@Override
public void copy(String document) {
throw new UnsupportedOperationException("Scanner cannot copy");
}
@Override
public void email(String document) {
throw new UnsupportedOperationException("Scanner cannot email");
}
}
✅ Following ISP
// GOOD: Segregated interfaces for specific capabilities
// Separate interfaces for each functionality
interface Printable {
void print(String document);
}
interface Scannable {
void scan(String document);
}
interface Faxable {
void fax(String document);
}
interface Copyable {
void copy(String document);
}
interface Emailable {
void email(String document);
}
// Simple printer only implements what it needs
class SimplePrinter implements Printable {
@Override
public void print(String document) {
System.out.println("Printing: " + document);
}
}
// Scanner only implements what it needs
class DocumentScanner implements Scannable {
@Override
public void scan(String document) {
System.out.println("Scanning: " + document);
}
}
// Multifunction printer implements multiple interfaces
class MultiFunctionPrinter implements Printable, Scannable, Copyable, Faxable {
@Override
public void print(String document) {
System.out.println("MFP Printing: " + document);
}
@Override
public void scan(String document) {
System.out.println("MFP Scanning: " + document);
}
@Override
public void copy(String document) {
System.out.println("MFP Copying: " + document);
}
@Override
public void fax(String document) {
System.out.println("MFP Faxing: " + document);
}
}
// Smart printer with email capability
class SmartPrinter implements Printable, Scannable, Emailable {
@Override
public void print(String document) {
System.out.println("Smart Printing: " + document);
}
@Override
public void scan(String document) {
System.out.println("Smart Scanning: " + document);
}
@Override
public void email(String document) {
System.out.println("Emailing: " + document);
}
}
// Usage example
public class OfficeEquipment {
public void printDocument(Printable printer, String doc) {
printer.print(doc);
}
public void scanDocument(Scannable scanner, String doc) {
scanner.scan(doc);
}
}
Advantages of ISP
- Flexibility: Classes implement only required functionality
- Maintainability: Changes to unused interfaces don't affect clients
- Clarity: Interfaces are focused and easy to understand
- Reduced coupling: Dependencies are minimized
5. Dependency Inversion Principle (DIP)
Definition
High-level modules should not depend on low-level modules. Both should depend on abstractions.
❌ Violating DIP
// BAD: High-level class directly depends on low-level classes
// Low-level classes
class MySQLDatabase {
public void save(String data) {
System.out.println("Saving to MySQL: " + data);
}
}
class EmailService {
public void sendEmail(String message) {
System.out.println("Sending email: " + message);
}
}
// High-level class with hard dependencies
class UserService {
private MySQLDatabase database; // Direct dependency on concrete class
private EmailService emailService; // Direct dependency on concrete class
public UserService() {
this.database = new MySQLDatabase(); // Tight coupling
this.emailService = new EmailService(); // Tight coupling
}
public void createUser(String userData) {
// Business logic
database.save(userData); // Depends on specific implementation
emailService.sendEmail("Welcome!"); // Depends on specific implementation
}
}
✅ Following DIP
// GOOD: Using abstractions and dependency injection
// Abstractions (interfaces)
interface Database {
void save(String data);
}
interface NotificationService {
void sendNotification(String message);
}
// Low-level implementations
class MySQLDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Saving to MySQL: " + data);
}
}
class PostgreSQLDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Saving to PostgreSQL: " + data);
}
}
class EmailService implements NotificationService {
@Override
public void sendNotification(String message) {
System.out.println("Sending email: " + message);
}
}
class SMSService implements NotificationService {
@Override
public void sendNotification(String message) {
System.out.println("Sending SMS: " + message);
}
}
class PushNotificationService implements NotificationService {
@Override
public void sendNotification(String message) {
System.out.println("Sending push notification: " + message);
}
}
// High-level class depends on abstractions
class UserService {
private final Database database;
private final NotificationService notificationService;
// Dependency injection through constructor
public UserService(Database database, NotificationService notificationService) {
this.database = database;
this.notificationService = notificationService;
}
public void createUser(String userData) {
// Business logic remains the same
database.save(userData);
notificationService.sendNotification("Welcome new user!");
}
}
// Usage with different implementations
public class Application {
public static void main(String[] args) {
// Can easily switch implementations
Database mysqlDb = new MySQLDatabase();
Database postgresDb = new PostgreSQLDatabase();
NotificationService emailService = new EmailService();
NotificationService smsService = new SMSService();
NotificationService pushService = new PushNotificationService();
// Different configurations
UserService userService1 = new UserService(mysqlDb, emailService);
UserService userService2 = new UserService(postgresDb, smsService);
UserService userService3 = new UserService(mysqlDb, pushService);
userService1.createUser("John Doe");
userService2.createUser("Jane Smith");
userService3.createUser("Bob Johnson");
}
}
// Factory pattern for dependency injection
class ServiceFactory {
public static UserService createUserService(String dbType, String notificationType) {
Database database = createDatabase(dbType);
NotificationService notificationService = createNotificationService(notificationType);
return new UserService(database, notificationService);
}
private static Database createDatabase(String type) {
switch (type.toLowerCase()) {
case "mysql": return new MySQLDatabase();
case "postgresql": return new PostgreSQLDatabase();
default: throw new IllegalArgumentException("Unknown database type: " + type);
}
}
private static NotificationService createNotificationService(String type) {
switch (type.toLowerCase()) {
case "email": return new EmailService();
case "sms": return new SMSService();
case "push": return new PushNotificationService();
default: throw new IllegalArgumentException("Unknown notification type: " + type);
}
}
}
Advantages of DIP
- Flexibility: Easy to swap implementations
- Testability: Can inject mock objects for testing
- Maintainability: Changes in low-level modules don't affect high-level modules
- Extensibility: New implementations can be added without changing existing code
Complete Example: E-commerce System
Here's a comprehensive example showing all SOLID principles working together:
// SRP: Each class has a single responsibility
public class Product {
private String id;
private String name;
private double price;
public Product(String id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// Getters
public String getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
}
// OCP: Open for extension, closed for modification
abstract class DiscountStrategy {
public abstract double calculateDiscount(double amount);
}
class RegularCustomerDiscount extends DiscountStrategy {
@Override
public double calculateDiscount(double amount) {
return amount * 0.05; // 5% discount
}
}
class PremiumCustomerDiscount extends DiscountStrategy {
@Override
public double calculateDiscount(double amount) {
return amount * 0.10; // 10% discount
}
}
class VIPCustomerDiscount extends DiscountStrategy {
@Override
public double calculateDiscount(double amount) {
return amount * 0.15; // 15% discount
}
}
// LSP: Subtypes must be substitutable for their base types
abstract class PaymentMethod {
public abstract boolean processPayment(double amount);
}
class CreditCardPayment extends PaymentMethod {
@Override
public boolean processPayment(double amount) {
System.out.println("Processing credit card payment: $" + amount);
return true;
}
}
class PayPalPayment extends PaymentMethod {
@Override
public boolean processPayment(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
return true;
}
}
// ISP: Segregated interfaces
interface OrderRepository {
void save(Order order);
Order findById(String id);
}
interface PaymentProcessor {
boolean processPayment(PaymentMethod method, double amount);
}
interface NotificationSender {
void sendOrderConfirmation(String customerEmail, Order order);
}
interface InventoryManager {
boolean isAvailable(String productId, int quantity);
void reserve(String productId, int quantity);
}
// DIP: Depend on abstractions, not concretions
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentProcessor paymentProcessor;
private final NotificationSender notificationSender;
private final InventoryManager inventoryManager;
// Dependency injection
public OrderService(OrderRepository orderRepository,
PaymentProcessor paymentProcessor,
NotificationSender notificationSender,
InventoryManager inventoryManager) {
this.orderRepository = orderRepository;
this.paymentProcessor = paymentProcessor;
this.notificationSender = notificationSender;
this.inventoryManager = inventoryManager;
}
public boolean createOrder(Order order, PaymentMethod paymentMethod) {
// Check inventory
for (OrderItem item : order.getItems()) {
if (!inventoryManager.isAvailable(item.getProductId(), item.getQuantity())) {
return false;
}
}
// Calculate total with discount
double total = order.calculateTotal();
double discount = order.getDiscountStrategy().calculateDiscount(total);
double finalAmount = total - discount;
// Process payment
if (!paymentProcessor.processPayment(paymentMethod, finalAmount)) {
return false;
}
// Reserve inventory
for (OrderItem item : order.getItems()) {
inventoryManager.reserve(item.getProductId(), item.getQuantity());
}
// Save order
orderRepository.save(order);
// Send confirmation
notificationSender.sendOrderConfirmation(order.getCustomerEmail(), order);
return true;
}
}
Summary
The SOLID principles provide a strong foundation for writing maintainable, flexible, and scalable Java applications. By following these principles:
- SRP ensures classes have focused responsibilities
- OCP allows extension without modification
- LSP maintains substitutability in inheritance
- ISP prevents interface pollution
- DIP reduces coupling through abstraction
Implementing SOLID principles leads to code that is easier to test, maintain, and extend, ultimately resulting in more robust software systems.
Top comments (0)