Share
Explore

KOTLIN Lab - Encapsulation with Getters and Setters - Simple Bank Operations

Below is an extended Code Lab that amplifies the use of getters and setters by simulating simple bank operations.
This lab introduces two interacting classes—Bank and Customer—to demonstrate class interactions, encapsulation, and getter/setter usage with validation.
The exercise focuses on a simple banking system where a Bank object manages a database of customer accounts, and a Customer object interacts with the Bank to perform withdrawals and deposits.
The lab builds on Lab 8’s encapsulation concepts, adding real-world context and class collaboration.

Code Lab: Encapsulation with Getters and Setters - Simple Bank Operations

Here is the complete code:


Objective: By the end of this lab, you will:
Implement encapsulation with private properties and custom getters/setters in two interacting classes. Simulate basic bank operations (withdrawals and deposits) with validation using getters and setters. Understand how objects of different classes can collaborate through method calls.
Concepts Covered:
public vs. private visibility modifiers with advanced getter/setter logic
Class and object interactions
Validation in setters for secure data manipulation]
1. Introduction (10 minutes)
Objective: Frame the lab around encapsulation and class interactions in a banking context.
Today, we’re amplifying getters and setters with a mini banking system!
We’ll use two classes—Bank and Customer—to show how objects work together.
Encapsulation isn’t just about hiding data—it’s about controlling how it’s accessed and changed.
Our Bank will protect account balances, and our Customer will interact with it safely.
You’ll build a system where customers can deposit and withdraw money, with getters and setters ensuring everything stays secure and valid.
Key Concepts:
Class Interactions: One object calling methods on another.
Encapsulation: private data with public getters/setters for controlled access.
Validation: Ensuring operations (e.g., withdrawals) respect rules (e.g., sufficient balance).
2. Designing the Classes
Objective: Introduce the Bank and Customer classes with encapsulation.
Step 1: The Bank Class
Purpose: Manages a simple database (a map) of customer names and chequing account balances.
Properties: A private accounts map.
Methods: Getters and setters to query and update balances.
class Bank { // Private map: key = customer name, value = balance private val accounts = mutableMapOf<String, Double>( "John" to 1000.0, // Pre-populated for testing "Alice" to 500.0 )
// Getter: Check if customer exists fun hasAccount(customerName: String): Boolean { return accounts.containsKey(customerName) }
// Getter: Retrieve balance (returns 0.0 if not found) fun getBalance(customerName: String): Double { return accounts[customerName] ?: 0.0 // Null-safe default }
// Setter: Update balance with validation fun setBalance(customerName: String, newBalance: Double) { if (newBalance >= 0) { accounts[customerName] = newBalance println("Balance updated for $customerName: $$newBalance") } else { println("Error: Balance cannot be negative for $customerName") } }
// Helper method for withdrawal (with getter validation) fun withdraw(customerName: String, amount: Double): Boolean { val currentBalance = getBalance(customerName) return if (amount > 0 && currentBalance >= amount) { setBalance(customerName, currentBalance - amount) true } else { println("Error: Insufficient funds or invalid amount for $customerName") false } }
// Helper method for deposit fun deposit(customerName: String, amount: Double) { if (amount > 0) { val currentBalance = getBalance(customerName) setBalance(customerName, currentBalance + amount) } else { println("Error: Deposit amount must be positive for $customerName") } } }
Step 2: The Customer Class
Purpose: Interacts with the Bank to manage a customer’s account. Properties: A private name and a reference to the Bank. Methods: Logic to query, withdraw, and deposit using the Bank’s getters/setters.
class Customer(private val name: String, private val bank: Bank) { // Getter for name fun getName(): String = name
// Check if account exists in bank fun checkAccount(): Boolean { return bank.hasAccount(name) }
// Perform banking operations if account exists fun performOperations() { if (checkAccount()) { println("Welcome, $name! Your balance is $${bank.getBalance(name)}") println("Options: 1. Withdraw 2. Deposit 3. Exit") val choice = readLine()?.toIntOrNull() ?: 3 // Simulated input
when (choice) { 1 -> { println("Enter amount to withdraw:") val amount = readLine()?.toDoubleOrNull() ?: 0.0 bank.withdraw(name, amount) println("New balance: $${bank.getBalance(name)}") } 2 -> { println("Enter amount to deposit:") val amount = readLine()?.toDoubleOrNull() ?: 0.0 bank.deposit(name, amount) println("New balance: $${bank.getBalance(name)}") } 3 -> println("Goodbye, $name!") else -> println("Invalid option") } } else { println("Sorry, $name, you don’t have an account with us.") } } }
3. Main Exercise: Simulate Bank Operations
Objective: Tie the classes together with a loop to simulate user interaction.
Instructions:
Write a program that:
Creates a Bank object with a pre-populated database.
Uses a while loop in main() to: Prompt for a username.
Create a Customer object with that name and the Bank.
Query the Bank and allow deposit/withdrawal if the account exists.
Uses getters and setters with validation (e.g., sufficient balance for withdrawals).
Step-by-Step Guide: Set Up the Program: Initialize the Bank and start a loop for user input.
fun main() { val bank = Bank() var continueBanking = true
println("=== Welcome to Simple Bank ===") while (continueBanking) { println("Enter your name (or 'exit' to quit):") val input = readLine() ?: "" // Simulated input if (input.lowercase() == "exit") { continueBanking = false } else { val customer = Customer(input, bank) customer.performOperations() } } println("Thank you for banking with us!") }
Test the Interaction: Simulate inputs like "John" (exists), "Bob" (doesn’t exist), and operations.
For simplicity, hardcode inputs in a playground or use readLine().
Sample Simulation (Hardcoded):
fun main() { val bank = Bank()
// Simulate John withdrawing $200 val john = Customer("John", bank) println("John’s initial balance: $${bank.getBalance("John")}") bank.withdraw("John", 200.0) println("After withdrawal: $${bank.getBalance("John")}")
// Simulate John depositing $50 bank.deposit("John", 50.0) println("After deposit: $${bank.getBalance("John")}")
// Simulate invalid withdrawal bank.withdraw("John", 1000.0)
// Simulate unknown customer val bob = Customer("Bob", bank) bob.performOperations() } Output:
John’s initial balance: $1000.0 Balance updated for John: $800.0 After withdrawal: $800.0 Balance updated for John: $850.0 After deposit: $850.0 Error: Insufficient funds or invalid amount for John Sorry, Bob, you don’t have an account with us. 4. Deep Dive: Getters and Setters in Action (15 minutes) Objective: Highlight how encapsulation and validation work.
Key Points: Bank.accounts is Private: Only the Bank class can touch the accounts map directly. Customer interacts via getBalance(), setBalance(), etc. Getter Validation: withdraw() uses getBalance() to check funds before proceeding. Setter Validation: setBalance() ensures no negative balances. deposit() and withdraw() enforce positive amounts. Class Interaction: Customer doesn’t know how Bank stores data—it just calls public methods, keeping the system modular. Experiment: Try bypassing encapsulation by making accounts public and accessing it directly from main(). What risks do you see? 5. Additional Challenges (10 minutes) Objective: Extend the system for practice.
Challenge 1: Add Interest
Add a Bank method addInterest(rate: Double) that increases all balances by a percentage.
fun addInterest(rate: Double) { for ((name, balance) in accounts) { setBalance(name, balance * (1 + rate / 100)) } }
Challenge 2: Transaction History
Add a private List<String> in Bank to log transactions, with a getter to view them.
Challenge 3: PIN (Simple Security)
Add a private pin to Customer and require it for operations (just a number for now).
6. Wrap-Up and Q&A (5 minutes) Objective: Recap and solidify lessons.
Key Takeaways: Encapsulation Protects: private hides data; getters/setters control access. Validation Ensures Safety: No negative balances or overdrafts. Class Interactions: Objects talk via public methods, not direct data access. Discussion Questions: "How does making age private in Student compare to accounts in Bank?" "Why validate in setters instead of trusting the caller?" "What happens if Customer could change accounts directly?" Sample Full Program Here’s the complete code:
class Bank { private val accounts = mutableMapOf<String, Double>( "John" to 1000.0, "Alice" to 500.0 )
fun hasAccount(customerName: String): Boolean = accounts.containsKey(customerName) fun getBalance(customerName: String): Double = accounts[customerName] ?: 0.0 fun setBalance(customerName: String, newBalance: Double) { if (newBalance >= 0) { accounts[customerName] = newBalance println("Balance updated for $customerName: $$newBalance") } else { println("Error: Balance cannot be negative for $customerName") } }
fun withdraw(customerName: String, amount: Double): Boolean { val currentBalance = getBalance(customerName) return if (amount > 0 && currentBalance >= amount) { setBalance(customerName, currentBalance - amount) true } else { println("Error: Insufficient funds or invalid amount for $customerName") false } }
fun deposit(customerName: String, amount: Double) { if (amount > 0) { val currentBalance = getBalance(customerName) setBalance(customerName, currentBalance + amount) } else { println("Error: Deposit amount must be positive for $customerName") } } }
class Customer(private val name: String, private val bank: Bank) { fun getName(): String = name fun checkAccount(): Boolean = bank.hasAccount(name)
fun performOperations() { if (checkAccount()) { println("Welcome, $name! Your balance is $${bank.getBalance(name)}") println("Options: 1. Withdraw 2. Deposit 3. Exit") val choice = readLine()?.toIntOrNull() ?: 3 when (choice) { 1 -> { println("Enter amount to withdraw:") val amount = readLine()?.toDoubleOrNull() ?: 0.0 bank.withdraw(name, amount) println("New balance: $${bank.getBalance(name)}") } 2 -> { println("Enter amount to deposit:") val amount = readLine()?.toDoubleOrNull() ?: 0.0 bank.deposit(name, amount) println("New balance: $${bank.getBalance(name)}") } 3 -> println("Goodbye, $name!") else -> println("Invalid option") } } else { println("Sorry, $name, you don’t have an account with us.") } } }
fun main() { val bank = Bank() var continueBanking = true
println("=== Welcome to Simple Bank ===") while (continueBanking) { println("Enter your name (or 'exit' to quit):") val input = readLine() ?: "" if (input.lowercase() == "exit") { continueBanking = false } else { val customer = Customer(input, bank) customer.performOperations() } } println("Thank you for banking with us!") } Sample Interaction (Simulated Input):
=== Welcome to Simple Bank === Enter your name (or 'exit' to quit): John Welcome, John! Your balance is $1000.0 Options: 1. Withdraw 2. Deposit 3. Exit 1 Enter amount to withdraw: 200 Balance updated for John: $800.0 New balance: $800.0 Enter your name (or 'exit' to quit): Bob Sorry, Bob, you don’t have an account with us. Enter your name (or 'exit' to quit): exit Thank you for banking with us! This lab amplifies getters and setters with a practical banking simulation, showcasing encapsulation, validation, and class interactions.
Want to print your doc?
This is not the way.
Try clicking the ⋯ next to your doc name or using a keyboard shortcut (
CtrlP
) instead.