Unlocking the Power of Encapsulation in Java: A Banking System Example
After a long break from posting on my blog, I’m thrilled to be back and more committed than ever to sharing valuable insights on Java programming. Moving forward, I’ll be exploring key concepts in Java through practical examples, starting with encapsulation. Let’s dive into this fundamental object-oriented programming principle with a real-world example.
Starting Point: A Basic BankAccount Class
Let’s begin with a simple BankAccount
class that incorporates the fundamental idea of encapsulation—grouping data and methods into a single unit, the class—but doesn’t yet fully unleash its key benefits. This class represents the account number and balance but exposes these details directly.
public class BankAccount {
public String accountNumber;
public double balance;
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
public void deposit(double amount) {
balance += amount;
}
public void withdraw(double amount) {
balance -= amount;
}
}
While functional, this design allows unrestricted access to accountNumber
and balance
, potentially leading to data integrity issues. Let’s now apply encapsulation to address these shortcomings.
Key Points
1. Data Hiding
Encapsulation allows the internal state of an object to be hidden from the outside world. This is achieved by declaring fields as private and providing controlled access through public getter and setter methods.
public class BankAccount {
private String accountNumber;
private double balance;
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
public String getAccountNumber() {
return accountNumber;
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
balance += amount;
}
public void withdraw(double amount) {
balance -= amount;
}
}
With this implementation, the fields accountNumber
and balance
are hidden from direct access, safeguarding the data integrity.
2. Improved Security
Sensitive data can be protected from unauthorized access or modification by controlling how it is accessed or changed. For example, validation can be added to ensure deposits and withdrawals are valid.
public class BankAccount{ private String accountNumber; private double balance; public BankAccount(String accountNumber, double initialBalance) { this.accountNumber = accountNumber; this.balance = initialBalance; } public String getAccountNumber() { return accountNumber; } public double getBalance() { return balance; } public void deposit(double amount) { if (amount > 0) { balance += amount; } else { System.out.println("Invalid deposit amount!"); } } public void withdraw(double amount) { if (amount > 0 && amount <= balance) { balance -= amount; } else { System.out.println("Invalid withdrawal amount or insufficient balance!"); } } }
Adding these checks ensures that the system behaves predictably and securely.
3. Modularity
Encapsulation promotes modularity by defining a clear boundary for what is accessible from outside the class, making it easier to maintain and modify the code. For instance, adding a password mechanism for certain operations introduces a clear modular design:
public class BankAccount { private String accountNumber; private double balance; private String password; public BankAccount(String accountNumber, String password, double initialBalance) { this.accountNumber = accountNumber; this.password = password; this.balance = initialBalance; } public String getAccountNumber() { return accountNumber; } public double getBalance(String enteredPassword) { if (authenticate(enteredPassword)) { return balance; } else { System.out.println("Invalid password! Access denied."); return -1; } } public void deposit(double amount) { if (amount > 0) { balance += amount; } else { System.out.println("Invalid deposit amount!"); } } public void withdraw(double amount, String enteredPassword) { if (authenticate(enteredPassword)) { if (amount > 0 && amount <= balance) { balance -= amount; } else { System.out.println("Invalid withdrawal amount or insufficient balance!"); } } else { System.out.println("Invalid password! Access denied."); } } private boolean authenticate(String enteredPassword) { return this.password.equals(enteredPassword); } }
Now, critical operations like retrieving the balance and withdrawing an amount are secure and modularized.
4. Flexibility and Maintenance
Encapsulation not only helps protect the integrity and modularity of your data but also offers remarkable flexibility and ease of maintenance. It allows developers to modify the internal workings of a class without disrupting the code that interacts with it, as long as the public interface (e.g., the getters and setters) remains the same. This is a crucial advantage, especially when dealing with complex, long-lived systems.
Practical Example: Enhancing a Banking System
Scenario#1: Enhancing Security
Now, let’s imagine that security concerns arise, and you want to add password encryption to the BankAccount
class. Without encapsulation, this change could result in widespread changes across the system. However, by modifying only the implementation details inside the class (while keeping the external interface the same), you can add encryption seamlessly.
Here’s how you could encrypt the password while preserving the class’s external behavior:
public class BankAccount { private String accountNumber; private double balance; private String encryptedPassword; public BankAccount(String accountNumber, String password, double initialBalance) { this.accountNumber = accountNumber; this.encryptedPassword = encryptPassword(password); this.balance = initialBalance; } public String getAccountNumber() { return accountNumber; } public double getBalance(String enteredPassword) { if (authenticate(enteredPassword)) { return balance; } else { System.out.println("Invalid password! Access denied."); return -1; } } public void deposit(double amount) { if (amount > 0) { balance += amount; } else { System.out.println("Invalid deposit amount!"); } } public void withdraw(double amount, String enteredPassword) { if (authenticate(enteredPassword)) { if (amount > 0 && amount <= balance) { balance -= amount; } else { System.out.println("Invalid withdrawal amount or insufficient balance!"); } } else { System.out.println("Invalid password! Access denied."); } } private String encryptPassword(String password) { // Simple encryption logic (placeholder) return "encrypted_" + password; } private boolean authenticate(String enteredPassword) { return this.encryptedPassword.equals(enteredPassword); } }
With this change, the authenticate()
method still works exactly the same way externally, but now the password is encrypted internally, enhancing security without impacting external functionality.
Scenario#2: Adding New Features
Imagine that you need to add several features to the BankAccount
class:
- Transaction logging for auditing purposes
- Transaction limits, such as a maximum withdrawal of $1,000 per day
- Notifications, such as sending an SMS or email after each transaction
These changes need to be added without disrupting the existing functionality. Thanks to encapsulation, you can do just that.
Here’s how you could update the BankAccount
class to include a transaction logging feature without modifying any external methods that interact with the class:
public class BankAccount { private String accountNumber; private double balance; private String encryptedPassword; public BankAccount(String accountNumber, String password, double initialBalance) { this.accountNumber = accountNumber; this.encryptedPassword = encryptPassword(password); this.balance = initialBalance; } public String getAccountNumber() { return accountNumber; } public double getBalance(String enteredPassword) { if (authenticate(enteredPassword)) { return balance; } else { System.out.println("Invalid password! Access denied."); return -1; } } public void deposit(double amount) { if (amount > 0) { balance += amount; } else { System.out.println("Invalid deposit amount!"); } } public void withdraw(double amount, String enteredPassword) { if (authenticate(enteredPassword)) { if (amount > 0 && amount <= balance) { balance -= amount; System.out.println("Withdrawn: " + amount); logTransaction("Withdraw", amount); // New feature } else { System.out.println("Invalid withdrawal amount or insufficient balance!"); } } else { System.out.println("Invalid password! Transaction denied."); } } private String encryptPassword(String password) { // Simple encryption logic (placeholder) return "encrypted_" + password; } private boolean authenticate(String enteredPassword) { return this.encryptedPassword.equals(enteredPassword); } // New logging method private void logTransaction(String type, double amount) { System.out.println(type + " of $" + amount + " logged."); } }
This change enhances security while keeping the external interface consistent.
5. Reusability
Encapsulation ensures that objects are self-contained and reusable in different parts of a program.
Simple Reusability
Encapsulation makes it easy to create and manage multiple instances of the same class with different data. For example:
public class Main {
public static void main(String[] args) {
BankAccount personalAccount = new BankAccount("12345", "personal123", 1000.00);
BankAccount businessAccount = new BankAccount("54321", "business456", 5000.00);
personalAccount.deposit(500.00);
businessAccount.withdraw(1000.00,
"business456");
System.out.println("Personal Account Balance: " +
personalAccount.getBalance("personal123"));
System.out.println("Business Account Balance: "
+
businessAccount.getBalance("business456"));
}
}
Advanced Reusability Using Inheritance
The BankAccount class can be extended for new features:
public class SavingsAccount extends BankAccount { private double interestRate; public SavingsAccount(String accountNumber,
String password, double initialBalance, double interestRate) { super(accountNumber, password, initialBalance); this.interestRate = interestRate; } public void addInterest(String enteredPassword) { double interest = getBalance(enteredPassword) * interestRate / 100;
deposit(interest); } }
The SavingsAccount class reuses the encapsulated functionality of the BankAccount class while adding new features
like interest calculation.
Conclusion
Encapsulation is a powerful tool for building secure, maintainable, and reusable classes. By hiding implementation details and providing controlled access to data, you can create systems that are both robust and flexible. Try applying these principles to your own projects, and see the difference it makes!