SOLID principles are a set of design guidelines in object-oriented programming that help developers create more maintainable, scalable, and robust software. They were introduced by Robert C. Martin (Uncle Bob) and are an acronym representing five key principles:

1. Single Responsibility Principle (SRP)

  • Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.
  • Purpose: To keep classes focused and manageable, making them easier to maintain.

Example: Consider a banking system with functionalities for user management and transaction logging.

Bad Example (violates SRP):

public class AccountService {
    public void createAccount(String accountHolder) {
        // Logic to create account
    }
    
    public void logTransaction(String details) {
        // Logic to log transaction details
    }
}

AccountService handles both account management and transaction logging, violating SRP.

Good Example (follows SRP):

public class AccountService {
    public void createAccount(String accountHolder) {
        // Logic to create account
    }
}

public class TransactionLogger {
    public void logTransaction(String details) {
        // Logic to log transaction details
    }
}

2. Open/Closed Principle (OCP)

  • Definition: Software entities (classes, modules, functions) should be open for extension but closed for modification.
  • Purpose: To allow the system to be extended without altering existing code, minimizing the risk of introducing bugs.

Example: Imagine a system that processes different types of transactions.

Bad Example (violates OCP):

public class TransactionProcessor {
    public void process(String transactionType) {
        if (transactionType.equals("Deposit")) {
            // Logic for deposit transaction
        } else if (transactionType.equals("Withdrawal")) {
            // Logic for withdrawal transaction
        }
    }
}

Adding a new transaction type requires modifying TransactionProcessor.

Good Example (follows OCP):

public interface Transaction {
    void process();
}

public class DepositTransaction implements Transaction {
    @Override
    public void process() {
        // Logic for deposit transaction
    }
}

public class WithdrawalTransaction implements Transaction {
    @Override
    public void process() {
        // Logic for withdrawal transaction
    }
}

public class TransactionProcessor {
    public void processTransaction(Transaction transaction) {
        transaction.process();
    }
}

3. Liskov Substitution Principle (LSP)

  • Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.
  • Purpose: To ensure that derived classes extend the base class without changing expected behavior.

Example: Assume a base BankAccount class and a derived FixedDepositAccount class.

Bad Example (violates LSP):

public class BankAccount {
    public void deposit(double amount) {
        // Logic for deposit
    }
    
    public void withdraw(double amount) {
        // Logic for withdrawal
    }
}

public class FixedDepositAccount extends BankAccount {
    @Override
    public void withdraw(double amount) {
        throw new UnsupportedOperationException("Withdrawals not allowed"); // Violates LSP
    }
}

Clients using BankAccount should not expect withdraw to throw an exception, breaking the LSP.

Good Example (follows LSP):

public abstract class BankAccount {
    public abstract void deposit(double amount);
}

public class SavingsAccount extends BankAccount {
    @Override
    public void deposit(double amount) {
        // Logic for deposit
    }

    public void withdraw(double amount) {
        // Logic for withdrawal
    }
}

public class FixedDepositAccount extends BankAccount {
    @Override
    public void deposit(double amount) {
        // Logic for deposit in a fixed deposit account
    }
}

4. Interface Segregation Principle (ISP)

  • Definition: A class should not be forced to implement interfaces it does not use. Instead of one large interface, multiple smaller interfaces are preferred.
  • Purpose: To ensure that classes implement only the functionality they need, promoting a more modular design.

Example: Suppose we are designing a machine interface for a multifunction device.

Bad Example (violates ISP):

public interface Machine {
    void print();
    void scan();
    void fax();
}

public class Printer implements Machine {
    @Override
    public void print() {
        // Printing logic
    }

    @Override
    public void scan() {
        // Does nothing
    }

    @Override
    public void fax() {
        // Does nothing
    }
}

Printer is forced to implement methods it does not need.

Good Example (follows ISP):

public interface Printer {
    void print();
}

public interface Scanner {
    void scan();
}

public class SimplePrinter implements Printer {
    @Override
    public void print() {
        // Printing logic
    }
}

public class MultiFunctionPrinter implements Printer, Scanner {
    @Override
    public void print() {
        // Printing logic
    }

    @Override
    public void scan() {
        // Scanning logic
    }
}

5. Dependency Inversion Principle (DIP)

  • Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.
  • Purpose: To decouple components in the system, making it easier to maintain and test.

Example: Suppose we have a notification system for sending alerts.

Bad Example (violates DIP):

public class EmailService {
    public void sendEmail(String message) {
        // Logic to send an email
    }
}

public class Alert {
    private EmailService emailService = new EmailService(); // High-level depends on low-level

    public void sendAlert(String message) {
        emailService.sendEmail(message);
    }
}

Good Example (follows DIP):

public interface NotificationService {
    void sendNotification(String message);
}

public class EmailService implements NotificationService {
    @Override
    public void sendNotification(String message) {
        // Logic to send an email
    }
}

public class Alert {
    private NotificationService notificationService;

    public Alert(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void sendAlert(String message) {
        notificationService.sendNotification(message);
    }
}

Alert depends on the NotificationService abstraction, not a specific implementation. This allows for easy switching to other implementations (e.g., SMS or push notifications).

Leave a Reply

Your email address will not be published. Required fields are marked *