The SOLID principles are key to writing flexible and maintainable code, especially in complex systems like those in the BFSI domain. Let’s break down these principles with examples that reflect common problems and solutions in banking and finance software.
What it means: A class should only have one reason to change. In other words, each class should do only one thing.
Problem:
In a banking app, a LoanProcessor class handles both loan processing and customer notifications. This violates SRP because it has two responsibilities.
public class LoanProcessor {
public void processLoan(Loan loan) {
// Loan approval logic
}
public void sendNotification(String message) {
// Notification logic
System.out.println("Notification sent: " + message);
}
}
Solution:
Separate the notification logic into its own class to adhere to SRP.
public class LoanProcessor {
public void processLoan(Loan loan) {
// Loan approval logic
}
}
public class NotificationService {
public void sendNotification(String message) {
// Notification logic
System.out.println("Notification sent: " + message);
}
}
What it means: Classes should be open for extension but closed for modification. This allows adding new functionality without changing existing code.
Problem:
A Transaction class processes different types of transactions. Adding a new transaction type like Transfer requires modifying the class.
public class Transaction {
public double process(String type, double amount) {
if (type.equals("credit")) {
return amount + 100; // Credit logic
} else if (type.equals("debit")) {
return amount - 100; // Debit logic
}
return 0;
}
}
Solution:
Use an interface for different transaction types, allowing for new transaction types without modifying existing classes.
public interface Transaction {
double process(double amount);
}
public class CreditTransaction implements Transaction {
@Override
public double process(double amount) {
return amount + 100; // Credit logic
}
}
public class DebitTransaction implements Transaction {
@Override
public double process(double amount) {
return amount - 100; // Debit logic
}
}
public class TransferTransaction implements Transaction {
@Override
public double process(double amount) {
return amount; // Transfer logic
}
}
What it means: Subclasses should be replaceable by their parent classes without affecting the program's correctness.
Problem:
An Ostrich subclass incorrectly overrides the fly() method inherited from Bird, violating LSP because an ostrich can't fly.
public class Payment {
public void processPayment(double amount) {
System.out.println("Payment processed: " + amount);
}
}
public class LoanRepayment extends Payment {
@Override
public void processPayment(double amount) {
// Loan repayment doesn't deduct from the user's account, violating expected behavior
throw new UnsupportedOperationException("Loan repayment cannot use this process");
}
}
Solution:
Make sure subclasses properly implement the parent class's functionality.
public abstract class Payment {
public abstract void processPayment(double amount);
}
public class CreditCardPayment extends Payment {
@Override
public void processPayment(double amount) {
System.out.println("Credit card payment processed: " + amount);
}
}
public class LoanRepayment extends Payment {
@Override
public void processPayment(double amount) {
System.out.println("Loan repayment processed: " + amount);
}
}
What it means: Don't force a class to implement methods it doesn't need. Instead, create specific interfaces for each responsibility.
Problem:
An AccountService interface handles account creation, closing, and investment services. Not all accounts need investment services, violating ISP.
public interface AccountService {
void createAccount();
void closeAccount();
void manageInvestments(); // Not all accounts need this
}
public class SavingsAccount implements AccountService {
@Override
public void createAccount() {
// Create account logic
}
@Override
public void closeAccount() {
// Close account logic
}
@Override
public void manageInvestments() {
// Not applicable for savings account
throw new UnsupportedOperationException();
}
}
Solution:
Split the interface into more specific ones.
public interface AccountService {
void createAccount();
void closeAccount();
}
public interface InvestmentService {
void manageInvestments();
}
public class SavingsAccount implements AccountService {
@Override
public void createAccount() {
// Create account logic
}
@Override
public void closeAccount() {
// Close account logic
}
}
public class InvestmentAccount implements AccountService, InvestmentService {
@Override
public void createAccount() {
// Create account logic
}
@Override
public void closeAccount() {
// Close account logic
}
@Override
public void manageInvestments() {
// Manage investments logic
}
}
What it means: High-level modules should not depend on low-level modules. Both should depend on abstractions.
Problem:
The LoanService depends directly on a FileLogger. If the logging mechanism changes, you’ll need to modify LoanService, violating DIP.
public class FileLogger {
public void log(String message) {
System.out.println("Log to file: " + message);
}
}
public class LoanService {
private FileLogger logger = new FileLogger();
public void approveLoan(Loan loan) {
// Loan approval logic
logger.log("Loan approved: " + loan.getId());
}
}
Solution:
Use an interface for logging to remove the direct dependency on the logging mechanism.
public interface Logger {
void log(String message);
}
public class FileLogger implements Logger {
@Override
public void log(String message) {
System.out.println("Log to file: " + message);
}
}
public class DatabaseLogger implements Logger {
@Override
public void log(String message) {
System.out.println("Log to database: " + message);
}
}
public class LoanService {
private Logger logger;
public LoanService(Logger logger) {
this.logger = logger;
}
public void approveLoan(Loan loan) {
// Loan approval logic
logger.log("Loan approved: " + loan.getId());
}
}
By applying SOLID principles to the BFSI domain, we can build better software that is more flexible, scalable, and maintainable. These principles help reduce dependencies, avoid unnecessary modifications, and create clean, reusable code.
Recap of SOLID principles:
SRP: Keep each class focused on a single responsibility.
OCP: Make your classes extendable without needing to modify them.
LSP: Subclasses should replace base classes without causing issues.
ISP: Split interfaces so that clients only implement what they need.
DIP: Depend on abstractions, not concrete implementations.