OOP in R,Simulating a banking system.

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:

  1. Classes Definition
  2. Class instances
  3. Methods:
  • Public
  • Private
  1. Introspection

Hypothetical example.

In this post,I will go through a simple banking system that enables its us to:

  1. Create accounts

  2. Make deposits

  3. Make withdrawals

  4. Request and pay loans.

  5. 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:

  1. 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.

  2. 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

  1. private : An optional list of private members, which can be functions and non-functions.To access these methods,we use the private$method or private$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.

George Oduor
Data Analyst

I’m a data enthusiast and problem solver with a passion for leveraging technology to drive innovation. With a background in data science and a keen interest in Fintech and AI, I’m dedicated to crafting data-driven solutions that empower businesses and individuals.