Introduction.
Object Oriented Programming has been the best way to write programs in any language.But why OOP technique?As a matter of fact our physical world is made up of objects with different qualities.OOP is based on the concept of “objects” which can contain data or code.
A good example can be an animal object with several properties like movement,sound,diet,life span etc.We can also talk about a “bank object” , which I will be considering in this post, with properties like customers,accounts and methods like accounts creation,loan repayments etc.
Why OOP in R.
Well,personally I begun my R journey with simple procedure oriented programming then I graduated to functional programming.Its only after being exposed to the goodies that come with structuring my scripts in an Object Oriented Way using python.R offers more than one method of writing Object Oriented programs such as S3,S4,R6 etc.In this post i will focus on using R6 Classes offered by the R6 package.This is because it gives an almost similar method Python uses.If you are not good with python,you have nothing to worry,all you need to know is how to write an R function and simple operations.
Much about OOP can be found here.
Some key concepts we are going to learn here are:
- Classes Definition
- Class instances
- Methods:
- Public
- Private
- Introspection
Hypothetical example.
In this post,I will go through a simple banking system that enables its us to:
Create accounts
Make deposits
Make withdrawals
Request and pay loans.
Simulate a simple USSD request in the console.
Excited?Lets proceed.
Required packages.
We will only need two packages for this project,namely ,tidyverse and R6.
Install these packages if you don have them already in your current R installation.
#install.packages("R6")
#install.packages("tidyverse")
# load the packages here
suppressWarnings(suppressPackageStartupMessages(library(R6)))
suppressWarnings(suppressPackageStartupMessages(library(tidyverse)))
Defining Classes
The focal point of OOP is a class which are used to create objects. A class is is the blueprint of an object.It literally describes describes what an object will be but separate from the object itself.The same class can be used to for creating multiple objects.
R6 Classes are created using the key word R6Class
.It contains functions called methods.The methods specify the behavior and actions that an object created with the same class can perform using its specif data.
Its important to take note of two most important arguments of the class:
classname
: Defines the class name.Not required for the class to work though,but as described in the package documentation,its useful for S3 method dispatch.public
: A list of public members, which can be functions (methods) and non-functions (fields).
Below is a simple way to create a class in R6.First ,lets call our class CBS-short for core banking system.
CBS <- R6Class(classname = "CBS",public = list(
accs = tibble("Full Name" = NA,"IDNo" = NA,'AccountNo' = NA,'AccountBalance' = NA,'LoanBalance' = NA),
bank_name = NA,
initialize = function(accs = NULL,bank_name) {
self$bank_name = bank_name
cat(paste(' --------- \n This is ',self$bank_name,'. \n --------- \n'))
}
))
In the above class i have defined a class CBS
with a method initialize()
.For someone coming from python,you can think of this as the __init__
method.It is called when an instance of the object class is created using the class name as function.In this method the self$attribute
can be used to set the initial value of an instance’s attributes.
Class instantiation.
Instances are objects built from classes which contains real data and objects.To instantiate an object of the class above we use, $new()
method as shown below.Lets call our bank ABCBank.
ABCBank <- CBS$new(bank_name = 'ABCBank')
## ---------
## This is ABCBank .
## ---------
Methods and class atributes.
Classes have other methods defined to add functionality to them.These methods are basically functions.
Class attributes on the other hand are created by assigning variables within the body of the class,eg ‘bank_name’ and ‘accs’ in the above example.They are accessible within the class.To access these attributes you will use the self$attribute parameter as explained above.
To build our core banking system ,we will need some methods to offer functionality.At this point you will need the knowledge of R functions.
Tl;dr
Account Creation method.
A client will need his/her First Name,Second Name and National ID Number for this.
create_account = function(FirstName=NULL,SecondName=NULL,IDNo=NULL) {
FirstName = readline(prompt = "Enter FirstName: ")
SecondName = readline(prompt = "Enter SecondName: ")
IDNo = readline(prompt = "Enter IDNo: ")
id_acc = private$account_setup(IDNo)
account_details = tibble("Full Name" = paste(FirstName,SecondName),"IDNo" = as.character(pluck(id_acc,'IDNo')),
'AccountNo' = pluck(id_acc,'accno'),'AccountBalance' = 0,'LoanBalance' = 0)
self$accs = bind_rows(self$accs,account_details) %>% filter(!is.na(IDNo))
cat(sprintf("Hi %s, welcome to ABCBank.We look foward to doing business with you!\nYour account number is %s",FirstName,account_details$AccountNo))
}
The above function simply creates a tibble data frame of the newly created account and adds it to the existing accounts.Other properties like private$
included will be explained bellow.
Deposit Method
In order for our clients to make deposits,we will require their loan accounts and amount to be deposited as shown in the function below.
make_deposit = function(acc=NULL,Amount=NULL){
acc = readline(prompt = "Enter acc ")
Amount = as.numeric(readline(prompt = "Enter Amount "))
private$check_acc(acc = acc,expr = {
self$accs = self$accs %>% mutate(AccountBalance = ifelse(AccountNo == acc,AccountBalance+Amount,AccountBalance))
cat(glue("Dear Customer,your deposit of Ksh {Amount} was received successfully.Your current balance is Ksh {
pull(filter(self$accs,AccountNo == acc),AccountBalance )}"))})
}
You can notice that a message is sent to the client after making the deposit.
Withdrawal method
make_withdrawal = function(acc=NULL,Amount=NULL){
acc = readline(prompt = "Enter acc ")
Amount = as.numeric(readline(prompt = "Enter Amount "))
private$check_acc(acc = acc,expr = {
info = filter(self$accs,AccountNo == acc)
if ( (pull(info,AccountBalance) - 100) > Amount ) {
self$accs <- self$accs %>% mutate(
AccountBalance = ifelse(AccountNo == acc,AccountBalance - Amount,AccountBalance)
)
}else{
message(paste(' ----------\n Dear ',pull(info,`Full Name`),',we are unable to process a your request due to low balance in your account.Please try a lower value'))
}
})
}
Quick overview of whats going on here: In order for a withdrawal to be successful ,correct account is supplied plus an amount which wont leave the account with less than Ksh 100.
Account Balance
To check account balance we need just the account number then following is just simple filter method from all the accounts.
check_balance = function(acc=NULL){
acc = readline(prompt = "Enter acc ")
private$check_acc(acc = acc,expr = {
balance = self$accs %>% filter(AccountNo == acc) %>% pull(AccountBalance)
message(glue("--------------\n Dear Customer,your account balance is Ksh {balance}.\n--------------"))
})
}
Loan Request Method
All this method does is simply to check if supplied account number is accurate,check if the client has existing loan to avoid multiple loan disbursements.
Notice that to access client database you need the self$accs
which has the list of all clients.
request_loan = function(acc=NULL,Amount=NULL){
acc = readline(prompt = "Enter acc ")
Amount = as.numeric(readline(prompt = "Enter Amount "))
private$check_acc(acc = acc,expr = {
info = filter(self$accs,AccountNo == acc)
if ( (pull(info,LoanBalance)) <= 0 ) {
self$accs = self$accs %>% mutate(LoanBalance = ifelse(AccountNo == acc,Amount,LoanBalance))
cat(glue("Dear Customer,your loan request of Ksh {Amount} was processed successfully.
Your outstanding loan balance is Ksh {pull(filter(self$accs,AccountNo == acc),LoanBalance )}"))
}else{
message(glue("Dear Customer,your loan request of Ksh {Amount} was not successfull.
Please clear your outstanding loan balance of Ksh {pull(filter(self$accs,AccountNo == acc),LoanBalance )}."))
}
})
}
Loan Repayment
Same as the above,we only need the account number and amount to be repaid.If the amount is more than the balance,the extra amount is added to the transnational account balance.
If the clients tries to repay a loan which inst existing ,the program shuts and the transaction is canceled.
Nice ,right?
repay_loan = function(acc=NULL,Amount=NULL) {
acc = readline(prompt = "Enter acc ")
Amount = as.numeric(readline(prompt = "Enter Amount "))
private$check_acc(acc = acc,expr = {
info = filter(self$accs,AccountNo == acc)
if ((pull(info,LoanBalance)) > 0 ) {
self$accs = self$accs %>% mutate(LoanBalance = ifelse(AccountNo == acc,LoanBalance-Amount,LoanBalance),
AccountBalance = ifelse((AccountNo == acc) & (LoanBalance<0),abs(LoanBalance)+AccountBalance,AccountBalance ),
LoanBalance = ifelse(LoanBalance < 0 ,0,LoanBalance))
if ( (pull(info,LoanBalance)) >= Amount ) {
cat(glue("Dear Customer,your loan payment of Ksh {Amount} was processed successfully.Your outstanding loan balance is Ksh {
pull(filter(self$accs,AccountNo == acc),LoanBalance )}"))
}else{
cat(glue("Dear Customer,your loan payment of Ksh {Amount} was processed successfully.Your loan is cleared,extra amount of Ksh {abs((pull(info,LoanBalance)) - Amount)} was deposited in your transactional account"))
}
}else{
message("Dear Customer,you do not have an existing loan.")
}
})
}
Checking Loan Balance
Works same as Account balance check.
check_loan_balance = function(acc=NULL){
acc = readline(prompt = "Enter acc ")
private$check_acc(acc = acc,expr = {
balance = self$accs %>% filter(AccountNo == acc) %>% pull(LoanBalance)
cat(glue("--------------\n Dear Customer,your loan account balance is Ksh {balance}.\n--------------"))
})
}
Simulating USSD in the console method
You would like the access all these methods with a single call of this class.We will access all the methods in the class using the self$method
parameter.
For efficiency purposes a new client who has just registered is taken to the menu straight away.
get_services = function(acc = NULL) {
if (is.null(acc)) {
cat('Welcome to ABC Banking Services!\nSelect\n1:Create new account\n2:Deposit\n3:Withdraw\n4:Request Loan\n5:Repay Loan\n6:My Account Balance\n7:My Loan Balance')
input <- readline(prompt="Enter here: ")
switch (as.character(input),
"1" = self$create_account(),"2" = self$make_deposit(),"3" = self$make_withdrawal(),
"4" = self$request_loan(),"5" = self$repay_loan(),"6" = self$check_balance(),"7" = self$check_loan_balance()
)
}else{
cat(paste('-----------\nSelect\n1:Deposit\n2:Withdraw\n3:Request Loan\n4:Repay Loan\n5:My Account Balance\n6:My Loan Balance'))
input <- readline(prompt="Enter here: ")
switch (as.character(input),
"1" = self$make_deposit(),"2" = self$make_withdrawal(),"3" = self$request_loan(),"4" = self$repay_loan(),
"5" = self$check_balance(),"6" = self$check_loan_balance()
)
}
}
Controlling access to methods.
R6 classes has two main ways of controlling access.These are part of the class arguments but are not
private
: An optional list of private members, which can be functions and non-functions.To access these methods,we use theprivate$method
orprivate$attribute
parameters.
In our models ,we can see that we called private$check_acc
and private$account_setup
methods.Lets explain what they do down here.
Private Methods : account_setup
This function generates new account numbers and checks if it is similar to any account previously generated.If this is true it generates again until a unique one is made.This ensures that the generated account number is unique.The function also check if a client tries to register twice with the same National ID Number.If so,the client is prompted to enter a new one.
account_setup = function(IDNo) {
if (nrow(filter(self$accs,!is.na(IDNo)) > 0 )) {
accno = paste0("00",substr(paste0(sample(1:100, 12, replace=F),collapse = ""),1,9))
while (accno %in% self$accs$AccountNo) {
accno = as.character(rstudioapi::showPrompt(title = "IDNo",message = "Enter here"))
}
}else{
accno = paste0("00",substr(paste0(sample(1:100, 12, replace=F),collapse = ""),1,9))
}
# check if user exists
if (nrow(filter(self$accs,!is.na(IDNo)) > 0 )) {
while (IDNo %in% self$accs$IDNo) {
IDNo = as.character(rstudioapi::showPrompt(title = "IDNo",message = "That ID already exists,Enter correct IDNo "))
}
}else{
IDNo = IDNo
}
out = list(
'IDNo' = IDNo,
'accno' = accno
)
return(out)
}
Private Methods : check_acc
For any transaction,we will need an account number.It is therefore important to check the validity of the account entered using one single function defined in the private method as shown.
check_acc = function(acc,expr={...}) {
if (acc %in% self$accs$AccountNo) {
expr
}else{
message("Account Supplied doesn't exist!")
}
}
Final Program
All the way up to this point,below is the all the methods and attribute combined in the class.
CBS <- R6Class(classname = "CBS",public = list(
accs = tibble("Full Name" = NA,"IDNo" = NA,'AccountNo' = NA,'AccountBalance' = NA,'LoanBalance' = NA),
bank_name = NA,
initialize = function(accs = NULL,bank_name) {
self$bank_name = bank_name
cat(paste(' --------- \n This is ',self$bank_name,'. \n --------- \n'))
},
create_account = function(FirstName=NULL,SecondName=NULL,IDNo=NULL) {
FirstName = readline(prompt = "Enter FirstName: ")
SecondName = readline(prompt = "Enter SecondName: ")
IDNo = readline(prompt = "Enter IDNo: ")
id_acc = private$account_setup(IDNo)
account_details = tibble(
"Full Name" = paste(FirstName,SecondName),
"IDNo" = as.character(pluck(id_acc,'IDNo')),
'AccountNo' = pluck(id_acc,'accno'),
'AccountBalance' = 0,
'LoanBalance' = 0
)
self$accs = bind_rows(self$accs,account_details) %>% filter(!is.na(IDNo))
cat(sprintf("Hi %s, welcome to ABCBank.We look foward to doing business with you!\nYour account number is %s",
FirstName,account_details$AccountNo))
self$get_services(acc = account_details$AccountNo)
# return(invisible(account_details))
},
make_deposit = function(acc=NULL,Amount=NULL){
acc = readline(prompt = "Enter acc ")
Amount = as.numeric(readline(prompt = "Enter Amount "))
private$check_acc(acc = acc,expr = {
self$accs = self$accs %>% mutate(AccountBalance = ifelse(AccountNo == acc,AccountBalance+Amount,AccountBalance))
cat(glue("Dear Customer,your deposit of Ksh {Amount} was received successfully.Your current balance is Ksh {
pull(filter(self$accs,AccountNo == acc),AccountBalance )}"))})
},
make_withdrawal = function(acc=NULL,Amount=NULL){
acc = readline(prompt = "Enter acc ")
Amount = as.numeric(readline(prompt = "Enter Amount "))
private$check_acc(acc = acc,expr = {
info = filter(self$accs,AccountNo == acc)
if ( (pull(info,AccountBalance) - 100) > Amount ) {
self$accs <- self$accs %>% mutate(
AccountBalance = ifelse(AccountNo == acc,AccountBalance - Amount,AccountBalance)
)
}else{
message(paste(' ----------\n Dear ',pull(info,`Full Name`),',we are unable to process a your request due to low balance in your account.Please try a lower value'))
}
})
},
check_balance = function(acc=NULL){
acc = readline(prompt = "Enter acc ")
private$check_acc(acc = acc,expr = {
balance = self$accs %>% filter(AccountNo == acc) %>% pull(AccountBalance)
message(glue("--------------\n Dear Customer,your account balance is Ksh {balance}.\n--------------"))
})
},
# loans
request_loan = function(acc=NULL,Amount=NULL){
acc = readline(prompt = "Enter acc ")
Amount = as.numeric(readline(prompt = "Enter Amount "))
private$check_acc(acc = acc,expr = {
info = filter(self$accs,AccountNo == acc)
if ( (pull(info,LoanBalance)) <= 0 ) {
self$accs = self$accs %>% mutate(LoanBalance = ifelse(AccountNo == acc,Amount,LoanBalance))
cat(glue("Dear Customer,your loan request of Ksh {Amount} was processed successfully.Your outstanding loan balance is Ksh {
pull(filter(self$accs,AccountNo == acc),LoanBalance )}"))
}else{
message(glue("Dear Customer,your loan request of Ksh {Amount} was not successfull.Please clear your outstanding loan balance of Ksh {
pull(filter(self$accs,AccountNo == acc),LoanBalance )}."))
}
})
},
repay_loan = function(acc=NULL,Amount=NULL) {
acc = readline(prompt = "Enter acc ")
Amount = as.numeric(readline(prompt = "Enter Amount "))
private$check_acc(acc = acc,expr = {
info = filter(self$accs,AccountNo == acc)
if ((pull(info,LoanBalance)) > 0 ) {
self$accs = self$accs %>% mutate(LoanBalance = ifelse(AccountNo == acc,LoanBalance-Amount,LoanBalance),
AccountBalance = ifelse((AccountNo == acc) & (LoanBalance<0),abs(LoanBalance)+AccountBalance,AccountBalance ),
LoanBalance = ifelse(LoanBalance < 0 ,0,LoanBalance))
if ( (pull(info,LoanBalance)) >= Amount ) {
cat(glue("Dear Customer,your loan payment of Ksh {Amount} was processed successfully.Your outstanding loan balance is Ksh {
pull(filter(self$accs,AccountNo == acc),LoanBalance )}"))
}else{
cat(glue("Dear Customer,your loan payment of Ksh {Amount} was processed successfully.Your loan is cleared,extra amount of Ksh {abs((pull(info,LoanBalance)) - Amount)} was deposited in your transactional account"))
}
}else{
message("Dear Customer,you do not have an existing loan.")
}
})
},
check_loan_balance = function(acc=NULL){
acc = readline(prompt = "Enter acc ")
private$check_acc(acc = acc,expr = {
balance = self$accs %>% filter(AccountNo == acc) %>% pull(LoanBalance)
cat(glue("--------------\n Dear Customer,your loan account balance is Ksh {balance}.\n--------------"))
})
},
# simulate USSD!
get_services = function(acc = NULL) {
if (is.null(acc)) {
cat('Welcome to ABC Banking Services!\nSelect\n1:Create new account\n2:Deposit\n3:Withdraw\n4:Request Loan\n5:Repay Loan\n6:My Account Balance\n7:My Loan Balance')
input <- readline(prompt="Enter here: ")
switch (as.character(input),
"1" = self$create_account(),
"2" = self$make_deposit(),
"3" = self$make_withdrawal(),
"4" = self$request_loan(),
"5" = self$repay_loan(),
"6" = self$check_balance(),
"7" = self$check_loan_balance()
)
}else{
cat(paste('-----------\nSelect\n1:Deposit\n2:Withdraw\n3:Request Loan\n4:Repay Loan\n5:My Account Balance\n6:My Loan Balance\n'))
input <- readline(prompt="Enter here: \n--------------\n")
switch (as.character(input),
"1" = self$make_deposit(),
"2" = self$make_withdrawal(),
"3" = self$request_loan(),
"4" = self$repay_loan(),
"5" = self$check_balance(),
"6" = self$check_loan_balance()
)
}
}),
private = list(
account_setup = function(IDNo) {
if (nrow(filter(self$accs,!is.na(IDNo)) > 0 )) {
accno = paste0("00",substr(paste0(sample(1:100, 12, replace=F),collapse = ""),1,9))
while (accno %in% self$accs$AccountNo) {
accno = as.character(rstudioapi::showPrompt(title = "IDNo",message = "Enter here"))
}
}else{
accno = paste0("00",substr(paste0(sample(1:100, 12, replace=F),collapse = ""),1,9))
}
# check if user exists
if (nrow(filter(self$accs,!is.na(IDNo)) > 0 )) {
while (IDNo %in% self$accs$IDNo) {
IDNo = as.character(rstudioapi::showPrompt(title = "IDNo",message = "That ID already exists,Enter correct IDNo "))
}
}else{
IDNo = IDNo
}
out = list(
'IDNo' = IDNo,
'accno' = accno
)
return(out)
},
check_acc = function(acc,expr={...}) {
if (acc %in% self$accs$AccountNo) {
expr
}else{
message("Account Supplied doesn't exist!")
}
}
))
Lets instantiate
ABCBank <- CBS$new(bank_name = "ABC Bank")
## ---------
## This is ABC Bank .
## ---------
Introspection
We can always inspect our classes using the class
function and use names
function to get a list of all methods contained in a class as shown below from an instanciation.
class(ABCBank)
## [1] "CBS" "R6"
We can see it shows that our instance is from the CBS class and it inherits from R6 class.
names(ABCBank)
## [1] ".__enclos_env__" "bank_name" "accs"
## [4] "clone" "get_services" "check_loan_balance"
## [7] "repay_loan" "request_loan" "check_balance"
## [10] "make_withdrawal" "make_deposit" "create_account"
## [13] "initialize"
All the methods discussed above are listed here.
Lets see a simple example of our system.
To use this system interactively,run the class above ,instantiate then call get_services
method.
ABCBank$get_services()
## Welcome to ABC Banking Services!
## Select
## 1:Create new account
## 2:Deposit
## 3:Withdraw
## 4:Request Loan
## 5:Repay Loan
## 6:My Account Balance
## 7:My Loan BalanceEnter here:
Finallly .
Its important to note that when defining a method in a class you do not use <-
operator for assignment but the standard =
sign.
I will write about class inheritance and and other OOP concepts in R in my next blog.You are free to copy this script and play with it in your R studio console.
All the best.