Problem Statement
Problem Statement
Lets implement an oversimplified example of a Banking system. This bank supports three types of Bank Accounts - Savings, Current & Loan.
Attributes
- Each type of bank account has the following attributes :
accountHolder: String
(name of account holder)balance: Float
(current balance in account)isActive: Boolean
(whether the account is active)openedOn: String
(date indd/mm/yyyy
format)transactions: List<Transaction>
(transactions of the account)
- Additionally,
- Savings account has one more attribute :
interestRate: Float
(interest rate in percentage)
- Current account has two more attributes :
firmName: String
firmRegistrationNo: String
- Loan account has four more attributes :
loanAmount: Float
loanPeriodInMonths: Int
loanInterestRate: Float
loanAmountPaid: Float
- Savings account has one more attribute :
- A Transaction is defined by following attributes :
date: String
amount: Float
description: String
Functionalities
Simulation of the following operations have to be implemented for all three type of accounts :
- Cash deposit (prevent if account is inactive)
- Cash withdrawal (prevent if insufficient balance or account inactive)
- Printing account statement with each transaction in the format :
$date → Rs. $amount : DEBIT / CREDIT → $description
- close account
Additionally,
- For savings account, simulate interest deposit
- For current account, simulate checking minimum balance and debit fine if found below that
- For loan account, simulate loan recovery / EMI payment and debit fine if insufficient funds
Examples
Examples
Savings Account
Savings Account
Input :
accountHolder = A
interestRate = 4%
Operations :
Withdraw Rs. 1000
Deposit Rs. 5000
Deposit Interest
Print Statement
Output :
Insufficient balance (Rs. 0.0) to withdraw (Rs. 1000.0)!
---- : ACCOUNT STATEMENT for A : ----
01/02/2023 -> Rs. 0.0 : CREDIT -> Opening Balance
01/02/2023 -> Rs. 5000.0 : CREDIT -> Cash Deposit
01/02/2023 -> Rs. 200.0 : CREDIT -> Interest for Feb 2023
01/02/2023 -> Rs. 5200.0 -> Closing Balance
Current Account
Current Account
Input :
accountHolder = B
firmName = ABC Enterprises
firmRegistrationNo = RJ14-123456
Operations :
Check Minimum Balance
Deposit Rs. 6000
Close Account
Withdraw Rs. 1000
Print Statement
Output :
Can't withdraw cash from an inactive account!
---- : ACCOUNT STATEMENT for B : ----
01/02/2023 -> Rs. 0.0 : CREDIT -> Opening Balance
01/02/2023 -> Rs. -100.0 : DEBIT -> Minimum balance Fine for Feb 2023
01/02/2023 -> Rs. 6000.0 : CREDIT -> Cash Deposit
01/02/2023 -> Rs. 5900.0 -> Closing Balance
Loan Account
Loan Account
Input :
accountHolder = C
loanAmount = 5,00,000
loanPeriodInMonths = 5 * 12
loanInterestRate = 8%
Operations :
Deposit Rs. 10,000
2 * Perform Loan Recovery
Deposit Rs. 20,000
4 * Perform Loan Recovery
Print Statement
Output :
Loan of Rs. 500000.0 @ 8.0% : EMI of Rs. 9000.0 : 3 / 60 paid
---- : ACCOUNT STATEMENT for C : ----
01/02/2023 -> Rs. 0.0 : CREDIT -> Opening Balance
01/02/2023 -> Rs. 10000.0 : CREDIT -> Cash Deposit
01/02/2023 -> Rs. -9000.0 : DEBIT -> Loan recovery (Feb 2023)
01/02/2023 -> Rs. -100.0 : DEBIT -> Insufficient balance for EMI (Feb 2023) Fine
01/02/2023 -> Rs. 20000.0 : CREDIT -> Cash Deposit
01/02/2023 -> Rs. -9000.0 : DEBIT -> Loan recovery (Feb 2023)
01/02/2023 -> Rs. -9000.0 : DEBIT -> Loan recovery (Feb 2023)
01/02/2023 -> Rs. -100.0 : DEBIT -> Insufficient balance for EMI (Feb 2023) Fine
01/02/2023 -> Rs. -100.0 : DEBIT -> Insufficient balance for EMI (Feb 2023) Fine
01/02/2023 -> Rs. 2700.0 -> Closing Balance
Naive approach
Naive approach
A naive approach to this problem is to create a common class - BankAccount
for all three types of accounts. But it leads to several issues which we will see in a while.
class BankAccount(
val accountType: String,
/* Common Fields */
val accountHolder: String,
var balance: Float,
var isActive: Boolean,
val openedOn: String,
val transactions: MutableList<Transaction>,
/* Savings Account Fields */
val interestRate: Float? = null,
/* Current Account Fields */
val firmName: String? = null,
val firmRegistrationNo: String? = null,
/* Loan Account Fields */
val loanAmount: Float? = null,
val loanPeriodInMonths: Int? = null,
val loanInterestRate: Float? = null,
var loanAmountPaid: Float? = null
) {
/* Common Functions */
fun deposit(amount: Float) { TODO() }
fun withdraw(amount: Float) { TODO() }
fun printStatement() { TODO() }
fun close() { TODO() }
/* Savings Account Functions */
fun depositInterest() { TODO() }
/* Current Account Functions */
fun checkMinimumBalance() { TODO() }
/* Loan Account Functions */
fun performLoanRecovery() { TODO() }
}
Note that we have created a new property for account type - accountType: String
which can take either of the following values SAVINGS
, CURRENT
and LOAN
.
We can set default values for the following properties :
class BankAccount(
/* ... */
var balance: Float = 0f,
var isActive: Boolean = true,
val openedOn: String = getCurrentDate(),
val transactions: MutableList<Transaction> = mutableListOf()
/* ... */
)
So, by default every account (if not specified)
- gets 0 balance,
- is active,
- gets
openedOn
date bygetCurrentDate()
function, transactions
is initialized to an empty list
Here is how to create each type of account :
// Creates a savings account
val savingsAccount = BankAccount(
accountType = "SAVINGS",
accountHolder = "A",
interestRate = 4f
)
// Creates a current account
val currentAccount = BankAccount(
accountType = "CURRENT",
accountHolder = "B",
firmName = "ABC Enterprises",
firmRegistrationNo = "RJ14-123456"
)
// Creates a loan account
val loanAccount = BankAccount(
accountType = "LOAN",
accountHolder = "C",
loanAmount = 5_00_000f,
loanAmountPaid = 0f,
loanPeriodInMonths = 5 * 12,
loanInterestRate = 8f
)
Problems in this approach
Problems in this approach
Invalid values for accountType
Invalid values for accountType
can be entered. accountType
is a String, so it can accept any value apart from SAVINGS
, CURRENT
and LOAN
also. For example, following code runs without any errors :
val account = BankAccount(
accountType = "STAR",
accountHolder = "A",
interestRate = 4f
)
We can solve this problem by adding a check for this property in init
block and making this field immutable using val
:
init {
val validAccountTypes = listOf("SAVINGS", "CURRENT", "LOAN")
if (accountType !in validAccountTypes) {
error("Invalid account type: ($accountType), must be one of $validAccountTypes")
}
}
No type based restrictions on properties
While creating a SAVINGS account object, one can easily define properties specific to CURRENT account :
val account = BankAccount(
accountType = "SAVINGS",
accountHolder = "A",
interestRate = 4f,
// CURRENT account properties
firmName = "ABC Enterprises",
firmRegistrationNo = "RJ14-123456"
)
firmName
& firmRegistrationNo
are specific to CURRENT
account only yet here we are able to define them for SAVINGS
account also. This leads to inconsistent data. There is no rigid boundary between accountTypes.
A workaround for this problem is to use private
Access Modifier & Constructor Overloading :
class BankAccount(
val accountType: String,
/* Common Fields */
val accountHolder: String,
var balance: Float = 0f,
var isActive: Boolean = true,
val openedOn: String = getCurrentDate(),
val transactions: MutableList<Transaction> = mutableListOf(
Transaction(getCurrentDate(), balance, "Opening Balance")
)
) {
/* Savings Account Fields */
private var interestRate: Float? = null
/* Current Account Fields */
private var firmName: String? = null
private var firmRegistrationNo: String? = null
/* Loan Account Fields */
private var loanAmount: Float? = null
private var loanPeriodInMonths: Int? = null
private var loanInterestRate: Float? = null
private var loanAmountPaid: Float? = null
/* Constructors */
// For SAVINGS account
constructor(accountHolder: String, interestRate: Float): this(
accountType = "SAVINGS",
accountHolder = accountHolder
) {
this.interestRate = interestRate
}
// For CURRENT account
constructor(accountHolder: String, firmName: String, firmRegistrationNo: String): this(
accountType = "CURRENT",
accountHolder = accountHolder
) {
this.firmName = firmName
this.firmRegistrationNo = firmRegistrationNo
}
// For LOAN account
constructor(
accountHolder: String,
loanAmount: Float,
loanPeriodInMonths: Int,
loanInterestRate: Float
): this(
accountType = "LOAN",
accountHolder = accountHolder
) {
this.loanAmount = loanAmount
this.loanPeriodInMonths = loanPeriodInMonths
this.loanInterestRate = loanInterestRate
loanAmountPaid = 0f
}
// ... Other functions
}
Note :
-
All common properties are arguments to the primary constrcutor.
-
For each accountType, we have a secondary constrcutor defined using constructor overloading. Also,
accountType
is automatically set in each secondary constructor. -
All the account-type specific properties are defined as
private
and inside the class body. This way, changing properties specific to accountType is now restricted :val savingsAccount = BankAccount( accountHolder = "A", interestRate = 4f ) // ERROR! Not allowed because firmName is private savingsAccount.firmName = "ABC Enterprises"
No type based restrictions on functions
Functions specific to SAVINGS
account can easily be invoked using a CURRENT
account object. For example, following code runs without any errors :
val currentAccount = BankAccount(
accountType = "CURRENT",
accountHolder = "B",
firmName = "ABC Enterprises",
firmRegistrationNo = "RJ14-123456"
)
currentAccount.depositInterest()
This shouldn’t happen, it is incorrect - interest should only be deposited in SAVINGS
account and not CURRENT
account!
Similarly, there is no clear boundary between all account types and hence gives rise to many logically incorrect operations.
One workaround for this problem would be to add GuardCode in all accountType-specific functions to check the type first. Example :
fun depositInterest() {
// GuardCode for type checking
if (accountType != "SAVINGS") {
error("This operation can only be performed on SAVINGS account.")
}
if (isActive) {
transact(balance * interestRate!! / 100, "Interest for ${getCurrentMonth()}")
}
}
Complex code
The common class approach has many problems and we have seen workarounds for each of them (GuardCode, Secondary constructors). These workarounds make the code complex. Moreover, we have only one class for all three account types making it hard to navigate across the code.
When more features are added, this complexity increases and hence decreases code readability & maintainability.
Conclusion
Conclusion
This problem would have been easy if account specific fields and functionalities weren’t there. For example, field interestRate
and functionality depositInterest()
are specific to Savings account only. Creating a common class exposes this field and functionality to non-Savings account also. Moreover interestRate
is not even defined for other accounts, then having it in the object of (say) Current account doesn’t make any sense.
Separate class approach
Separate class approach
The major issue with common class approach was of no clear boundaries between each of the accountTypes. So, lets define separate classes for each of the accountType.
class SavingsAccount(
/* Common Fields */
val accountHolder: String,
var balance: Float,
var isActive: Boolean,
val openedOn: String,
val transactions: MutableList<Transaction>,
/* Savings Account Fields */
val interestRate: Float
) {
/* Common Functions */
fun deposit(amount: Float) { TODO() }
fun withdraw(amount: Float) { TODO() }
fun printStatement() { TODO() }
fun close() { TODO() }
/* Savings Account Functions */
fun depositInterest() { TODO() }
}
class CurrentAccount(
/* Common Fields */
val accountHolder: String,
var balance: Float,
var isActive: Boolean,
val openedOn: String,
val transactions: MutableList<Transaction>,
/* Current Account Fields */
val firmName: String,
val firmRegistrationNo: String
) {
/* Common Functions */
fun deposit(amount: Float) { TODO() }
fun withdraw(amount: Float) { TODO() }
fun printStatement() { TODO() }
fun close() { TODO() }
/* Current Account Functions */
fun checkMinimumBalance() { TODO() }
}
class LoanAccount(
/* Common Fields */
val accountHolder: String,
var balance: Float,
var isActive: Boolean,
val openedOn: String,
val transactions: MutableList<Transaction>,
/* Loan Account Fields */
val loanAmount: Float,
val loanPeriodInMonths: Int,
val loanInterestRate: Float,
var loanAmountPaid: Float = 0f
) {
/* Common Functions */
fun deposit(amount: Float) { TODO() }
fun withdraw(amount: Float) { TODO() }
fun printStatement() { TODO() }
fun close() { TODO() }
/* Loan Account Functions */
fun performLoanRecovery() { TODO() }
}
To create each type of account, we write :
val savingsAccount = SavingsAccount(
accountHolder = "A",
interestRate = 4f
)
val currentAccount = CurrentAccount(
accountHolder = "B",
firmName = "ABC Enterprises",
firmRegistrationNo = "RJ14-123456"
)
val loanAccount = LoanAccount(
accountHolder = "C",
loanAmount = 5_00_000f,
loanPeriodInMonths = 5 * 12,
loanInterestRate = 8f
)
Advantage
Advantage
This approach helps us solve the problems faced in common class approach in a cleaner way. We now have separate classes for each accountType, so clear boundaries between all and hence no data inconsistency.
Problems
Problems
This solution leads to another major problem - Duplicate Code, No code reusability & Low Maintainability.
Note that the common fields and functions have to defined for each class separately. This is duplicate code and we are unable to reuse the same logic in each class. This leads to Low mainatainability. When adding/modifying/removing a common fields/functionality from BankAccount, we’ll have to make the same changes across all three files. Turns out, this approach also fails.
Inheritance approach
Inheritance approach
Common functionality & need for code reusability indicate a perfect usecase for Inheritance. In inheritance, we define a Parent class which contains all the logic that is supposed to be re-used.We can then define multiple Child classes that can reuse the logic defined in the parent class.
For the BankAccount example, we define a Parent class BankAccount
with all the common fields and functions :
open class BankAccount(
val accountHolder: String,
var balance: Float,
var isActive: Boolean,
val openedOn: String,
val transactions: MutableList<Transaction>
) {
constructor(account: BankAccount) : this(
account.accountHolder,
account.balance,
account.isActive,
account.openedOn,
account.transactions
)
fun deposit(amount: Float) { TODO() }
fun withdraw(amount: Float) { TODO() }
fun printStatement() { TODO() }
fun close() { TODO() }
}
Note :
- The use of
open
keyword makes the class eligible for Inheritance. - Each Child class constructor must invoke Parent class constructor for initializing its fields. For this, we have created a Copy Constructor that takes an object of BankAccount and initializes all properties based on that object. We’ll learn more about this topic in the following sections.
Now lets define three Child classes - SavingsAccount
, CurrentAccount
& LoanAccount
all of which inherit from BankAccount
class :
class SavingsAccount(
account: BankAccount,
val interestRate: Float
) : BankAccount(account) {
fun depositInterest() { TODO() }
}
class CurrentAccount(
account: BankAccount,
val firmName: String,
val firmRegistrationNo: String
) : BankAccount(account) {
fun checkMinimumBalance() { TODO() }
}
class LoanAccount(
account: BankAccount,
val loanAmount: Float,
val loanPeriodInMonths: Int,
val loanInterestRate: Float,
var loanAmountPaid: Float = 0f
) : BankAccount(account) {
fun performLoanRecovery() { TODO() }
}
Note :
- We use
: ParentClassName()
to define the Parent class - Each Child class constructor takes a BankAccount object -
account
as an argument (not as a field). It is then passed to Parent class constructor for initializing its fields :: BankAccount(account)
To create each type of account, we write :
val savingsAccount = SavingsAccount(
BankAccount("A"),
interestRate = 4f
)
val currentAccount = CurrentAccount(
BankAccount("B"),
firmName = "ABC Enterprises",
firmRegistrationNo = "RJ14-123456"
)
val loanAccount = LoanAccount(
BankAccount("C"),
loanAmount = 5_00_000f,
loanPeriodInMonths = 5 * 12,
loanInterestRate = 8f
)
Note that for creating an object of (say) SavingsAccount
, we first create an object of BankAccount
(Parent class) and pass it to SavingsAccount
constructor.
Advantages
Advantages
The Inheritance approach overcomes the limitations of not only common class approach but also separate class approach.
- There is now a clear boundary between each of the accountTypes.
- There is Code reusability - we define common functionalities only once. And this can be reused multiple times using inheritance.
- Better Maintainability - Adding, modifying or removing common field/functionality is now easy. We just have make changes in parent class and all child classes automatically adapt to those changes.
Conclusion
Conclusion
We had some common fields and functions that had to be re-used for SavingsAccount
, CurrentAccount
& LoanAccount
. Inheritance provided an intuitive way of reusing code this code across all accountTypes.