DEV Community

Dev Cookies
Dev Cookies

Posted on

SOLID Principles in Java: A Complete Guide with Code Examples

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 + ")";
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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() + ")";
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Summary

The SOLID principles provide a strong foundation for writing maintainable, flexible, and scalable Java applications. By following these principles:

  1. SRP ensures classes have focused responsibilities
  2. OCP allows extension without modification
  3. LSP maintains substitutability in inheritance
  4. ISP prevents interface pollution
  5. 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)