SOLID Principles
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).