It happens to be a powerful and expressive language by any standard. This blog posts details my very first Ruby project. Just like the title says, it would be a replica of the ATM (Automated Teller Machine) somewhere down your street.

USECASES

  • There are users.
  • Users have accounts - one account per head.
  • Accounts have balances - total available balance and minimum balance.
  • Accounts have passwords.
  • There must be a commandline Interface.

From the above, we sure know we'd need some classes. Here are the ones we would be creating :

  • Prompter -> Ask the user some questions via the cli.
  • Customer -> A user - this includes his account information.<sup>1</sup>
  • Atm -> This is the main container for our app.

Security must be built into any app. Ours isn't an exception. We want to make sure there are only authorized usage of our application. No trolls. Else we end up in some serious legal suit. Authentication is done by providing a valid debit card and a matching password - for the card. Hence to save passwords, we'd be making use of a database - of a sort. A (flat) file would serve as our database. This is for several reasons :

  • Ruby is a good language for text processing, this is an opportunity to demonstrate that strength.
  • We are not enterprise level

Our db file would contain some data in a specialized format our app understands. Each line would represent a user which would ultimately be converted into a Customer object.

Here is what a sample line looks like : 5011; 0000-1234-5678-5011; The Real Clown; ?2313! ; 60_000; 3_000. They are in the following order when delimited by ; :

  • The last 4 digits of a user's debit card.
  • The 16 digits of a user's debit card.
  • The user's full name.
  • The total amount (s)he has in the account.
  • The minimum amount that must be left in the account - Bank policy.

Since we are working on a cli app, we need to ask for certain inputs from the user which we then respond to. Here is time to put our Prompter to work.

class Prompter

  def prompt(question)
    puts question
    gets.strip
  end
end

gets is built in function that reads input from the keyboard. We show the user a question, then we wait till we get a reply. This is more like What would you like to do question we get on a real life machine.

Next up is the app container itself

class Atm

  FilePath = 'db.txt'

  Separator = ';' #separator for the data in the database file

  {% raw %}
  ##Here are the command the ATM understands
  {% endraw %}
  Balance = 0
  Withdraw = 1
  Logout = 2

  def initialize(prompter)
    @all_customers = []
    @last_fours = []
    @current_customer = nil
    @cli_prompter = prompter
    get_all_customer_details
  end
end

In Ruby, the constructor method is initialize not __construct like in PHP or the class name as in Java. Basic OO - We inject the prompter into the class, this is so as we can switch to something more sophisticated when we reach enterprise level.

  def start

    last_4_digits = @cli_prompter.prompt('ENTER the last 4 digits of your card ?')

    raise AtmRunTimeError, 'The digits must be 4 characters long' unless last_4_digits.length.eql? 4

  end

  protected

  def get_all_customer_details
    File.open(FilePath, 'r').each do |line|
      next unless line.match(/\w+/)
      @all_customers.push(line.strip)
    end
  end

The get_all_customer_details is simply fetching everything from the database and dumping them into an array in our app. This is so as we can get easily get all the data for the users without constantly digging into the file. The line that says /\w+/ is a regular expression that tells us to only get lines that contain words from the file (our database). Remember this method was called in the constructor.

Our start method tells our atm to do some real work. First we ask for the last four digits om his/her card. This is always unique so it helps us simulate a card insertion (into the slot). Then we throw an exception if the user provides some digit more or less than 4 in it's length - raise is Ruby's equivalent to PHP or Java's throw statement.

   def start
    #previous code

    current_customer = get_customer_details_by_last_four_digits last_4_digits.to_i #method call here. Parenthesis are totally optional

    end

  def get_customer_details_by_last_four_digits(number)
    found = []

    @all_customers.each do |customer|
      next unless customer.slice(0, 4).to_i.eql? number
      found = customer.split Separator
    end

    raise UnknownCardError, 'Invalid debit card' if found.empty?

    found
  end

There is something interesting in the get_customer_details_by_last_four_digits method. This method shows us many nice stuffs about the ruby language :

  • Everything is an object.
  • Reads a lot like regular english
  def start

    ###previous code

    begin

      is_password_valid(current_customer, @prompter.prompt('Please provide your password ?'))

      puts '', 'Authenticating you via our secure server'

      login_error_count = 0
      @all_customers = nil

      hydrate_data(current_customer)

      puts 'You have been authenticated', ''

    rescue InvalidPasswordError => e

     ###In other to prevent users from taking our app down with too many requests, we shut them out after 3 invalid password entries
      raise LoginThrottleError, e.message + '. Atm would exit now' if login_error_count >= 3

     ###Increment the error count, then re-prompt the user for a password
      login_error_count += 1

      retry
    end

    bootstrap_atm_commands # if we get here, we golden.
  end

  def is_password_valid(customer, password)
      raise InvalidPasswordError, 'Please input the right password' unless customer[3].strip.eql? password
  end

  def process_command(command)
    case command.to_i
      when Balance
        puts "Available Balance -> #{@current_customer.available_balance}", ''

      when Withdraw
        puts ''

        amount_to_withdraw = @prompter.prompt('How much would you like to withdraw ?').to_f

        if @current_customer.can_withdraw?(amount_to_withdraw)
          puts 'Authenticating your withdrawal'
          @current_customer.withdraw!(amount_to_withdraw)
          puts 'Done'
        else
          puts 'Insufficient funds!', ''
        end

      when Logout
        puts 'Unauthenticating you via our secure sever', 'You have been successfully logged out'

        exit

      else
        puts '', 'Unknown Command', ''
    end

    bootstrap_atm_commands
  end


  def bootstrap_atm_commands
    print_instructions
    process_command(@prompter.prompt('How may we help you today ? Please Enter a command'))
  end

  def hydrate_data(customer)
    @current_customer = Customer.new(customer[2].strip, customer[4].strip.to_f, customer[5].strip.to_f)
  end

  def print_instructions
    puts "Hello, #{@current_customer.full_name}", ''

    commands = [
        [Balance, 'check your balance'], [Withdraw, 'withdraw some cash'], [Logout, 'logout']
    ]

    commands.each { |key, value| puts "Press #{key} to #{value}." }

    puts ''
  end

<sup>2</sup>

This is quite large but quite self explanatory. We check if the user provided the right password. If yes, save the current user to the instance variable current_customer - which is actually more a sort of session stuff.

The bootstrap_atm_commands simply print some information to the screen while waiting for the user to enter some response, so as to perform the requested action. Our atm understands some basic commands - 0 for account balance, 1 to withdraw some cash (this command in turn prompts the user to specify how much he'd like to withdraw), 2 is for logging out.

The most interesting parts here are the extra methods we called on the customer instance - @current_customer.can_withdraw? and @current_customer.withdraw!. The method can_withdraw would return a boolean which is actually why it has a ? in it's method definition - a standard ruby practice by the way WHILE the withdraw! method would actually reduce the balance of the customer.

Great!!! But what does the Customer object look like ? Nothing complex if you ask.

class Customer

  attr_accessor :full_name, :available_balance

  def initialize(full_name, available_balance, minimum_balance)
    @full_name = full_name
    @available_balance = available_balance
    @minimum_balance = minimum_balance
  end

  def withdraw!(amount)
    @available_balance -= amount
  end

  def can_withdraw?(amount)
    #some banks have a minimum balance policy. Let's put that in perspective too.
    cannot_withdraw = (@available_balance - amount) > @minimum_balance
    cannot_withdraw ||= amount < @available_balance
  end
end

For the curious minded, here are the exceptions definition

AtmRunTimeError = Class.new(RuntimeError)

BalanceOverflowError = Class.new(ArgumentError)

class FraudError < AtmRunTimeError
end

class UnknownCardError < FraudError
end

class InvalidPasswordError < FraudError
end

LoginThrottleError = Class.new(RuntimeError)

With this, our application is complete and works. Tests should be written obviously especially the Customer object since we have more fine grained control over it - unlike others where there is a lot of prompting and reading stuffs from the keyboard.

To test out the atm, a dummy file - say app.rb - should be created with the following content :

#!/usr/bin/env ruby

require './lib/atm'
require './lib/prompter'
require './lib/exceptions'
require './lib/customer'

atm = Atm.new(Prompter.new)
atm.start

Make sure you make the file executable, then run ./app.rb. Or just ruby app.rb

Footnotes

1 Our app has one account per head. This is by design. In the real world, users can have multiple account.

2 Our Atm class obviously goes against SRP (Single Responsibility Principle). It does password validation via the is_password_valid method. It loads all users from the database file (get_all_customer_details method). This two operations can be refactored into their own specific classes - say a PasswordValidatorService and FileReader (or something).