Abstract Data Types, implemented as C++ classes, define our approach towards designing and building large systems. Clearly, a large system might involve a combination of many classes and complex classes may be constructed out of simpler ones. This chapter explores these so called "using relationships" among classes and introduces some features of C++ that are necessary to support them.
The chapter also gives some further insights into how to analyse complex problems and approach the design of modular programs.
The chapter is structured around the example of a simplified cashpoint machine which allows users to carry out transactions on bank accounts. In turn, the bank accounts make use of the Money class defined in the previous chapter.
The goal of our example is to simulate the operation of a cashpoint machine. In order to make the problem manageable, we will make some major simplifications to the real-world scenario.
The first, and often most difficult, stage of solving a problem like this is to analyse the task and derive an overall program structure. In the last chapter we discussed an approach towards designing class interfaces based around five categories of method (interface function). However, this assumes that we already know which classes we want. In this example we start a stage further back - we need to identify the correct classes for our program before we can specify their interfaces. In other words there is an analysis stage before a detailed design stage.
As with the last chapter, there is no magic formula to be applied. However, as a starting point we can try writing a brief summary of what the cashpoint simulator does. The summary consists of short sentences with all "nouns" and "verbs" highlighted (in this case using bold and italicised type respectively).
The nouns suggest possible classes, data members, function arguments and results for the program (cashpoint, user, account, money, balance, account number and PIN).
The verbs suggest possible methods for classes (access, withdraw, request-balance, check).
The next stage is to decide which nouns represent classes (i.e. types of general object) and which represent data members of these classes. My decision was that Cashpoint, Account and Money look like classes; balance, account number and PIN look like data members of account; and that the users are something outside of the program.
Let us now write a brief description of each class and identify some initial methods.
Of course, this analysis is very basic. The following design stage will involve fleshing out each class with a complete complement of methods and data members to make it sufficiently general to solve the problem (and also to potentially be re-used in future problems).
We have already seen the design of the Money class. The rest of the chapter looks at the design and implementation of the Account and Cashpoint classes.
Let us briefly apply the five categories of interface function to the Account class:
This leads to the following definition file (Account.h).
// Account.h - definition of a class to represent simple bank accounts // No interest paid! // Steve Benford Novemer 1991 #ifndef ACCOUNT_H #define ACCOUNT_H #include "Money.h" // use the Money class class Account { private: int account_id; // account number Money bal; // current balance int PIN; // Personal Identification Number public: // constructor Account(); // create an account // with balance of 0 and no initial PIN // access int set_account_id(int id); // set the account id for this account int get_account_id(); // return the account number int allocate_pin(); // allocate a new PIN number and return it int check_pin(int p); // check supplied PIN number - return true/false void credit(Money m); // increase balance by amount m int debit(Money m); // decrease balance by amount m // and return the amount debited Money balance(); // return current balance // I/O void print(); // print account details }; #endif
Notice that the Account class contains a data member of type Money. Here we see one possible relationship between classes - one class can be built on another. Note also that the Money class appears in arguments and results of Account's methods. This means that any program using the Account class should know how to use the Money class as well.
The design carefully ensures that the PIN number is only released outside the class once, when it is first allocated (so the account owner can be told what it is). It is allocated internally and can be checked, but not read, by other programs. Here we are using the encapsulation property of classes to increase security.
Here is the implementation of the Account class.
// Account.c - implementation of account class #include <stream.h> #include <stdlib.h> #include "Account.h" #include "Money.h" // construct a new account with a balance of 0 and no initial PIN number // notice how Account calls the constructor for Money Account::Account(): bal(0,0) { PIN = 0; } // set the id for this account int Account::set_account_id(int id) { account_id = id; } // return the account number for this account int Account::get_account_id() { return account_id; } // allocate a random PIN number in the range 1000 to 9999 int Account::allocate_pin() { PIN = (rand() % 9000) + 1000; // tell the outside world what the PIN was // the only time it goes outside this account! return PIN; } // check the given number against the PIN // notice that the PIN is not returned int Account::check_pin(int p) { if (p == PIN) return 1; else return 0; } // credit an amount m to the account void Account::credit(Money m) { bal.increase(m); } // debit an amount m from the account (no overdrafts!) // return success or failure int Account::debit(Money m) { if(m.less_equals(bal)) { bal.decrease(m); return 1; } else return 0; } // return the current balance Money Account::balance() { return bal; } // print account details (but not the PIN) void Account::print() { cout << "Account id: " << account_id << " Balance: "; bal.print(); }
Several things need to be explained about this implementation.
The function allocate_pin() allocates a random pin number in the range 1000 to 9999. It uses the standard function rand() to generate a random number in the range 0 to RAND_MAX (usually 32767) and then modifies this to get it in the desired range (note how the remainder operation, %, is used). If you want to use this function in your own programs you need to include the header file <stdlib.h>.
See how the credit and debit functions call methods on the balance object (and also how debit won't let the account go overdrawn).
Look at the constructor function for Account. It has to initialise data members including bal, an object of class Money. This requires special treatment - the constructor for the data member bal is called directly after the function heading before its main body.
In general, THE CONSTRUCTOR FOR A CLASS HAS TO EXPLICITLY CALL THE CONSTRUCTORS FOR ANY CONTAINED CLASSES. These calls are made directly after the function heading (following a colon) and must take place in the order that the data members are declared in the class definition.
The one exception to this rule is if the constructor for the inner object has no arguments (e.g. the Counter() class from chapter 2). In this case it will be called automatically without the programmer having to worry.
It is useful to be aware that objects are constructed from the inside outwards - this means that the constructors for any inner objects are called before those for the outer object. This is illustrated by the following example program.
// demonstrate constructors for contained classes #include <stream.h> class Jim { private: int a; public: Jim(int i); // rest of class ... }; Jim::Jim(int i) { cout << "Jim's constructor called\\n"; a = i; } class Jane { private: Jim a; int b; public: Jane(int i, int j); // rest of class ... }; Jane::Jane(int i, int j): a(i) { cout << "Jane's constructor called\\n"; b = j; } main() { Jane(1,2); }
Observe how the constructor for Jane has to call that for Jim in order to build the data member "a".
The following output is generated when the program is compiled and run, showing the order in which the various constructor functions are called.
Jim's constructor called Jane's constructor called
The definition of the Cashpoint class is as follows.
// Definition for a cashpoint controller class which looks after a set // of accounts and manages withdrawals and balance requests // statement and cheque book requests are not supported in this example // the example also doesn't consider how accounts are created and closed // or how different cashpoints can be used to access the same account // Steve benford - November 1991 #ifndef CASHPOINT_H #define CASHPOINT_H // use the account class #include "Account.h" const int MAX_ACCOUNTS = 10; // maximum number of accounts // status codes returned by cashpoint functions const int SUCCESS = 1; // operation carried out successfully const int BUSY = -1; // cashpoint currently dealing with another account const int AUTH_FAILURE = -2; // users PIN not verified const int NO_ACCOUNT_OPEN = -3; // can't do operation as no account is open const int INSUFFICIENT_FUNDS = -4; // not enough money for withdrawal const int NO_SUCH_ACCOUNT = -5; // there is no account for the given id class Cashpoint { private: Account accounts[MAX_ACCOUNTS]; // managed accounts int current; // position of current account // being dealt with (-1 = none) public: Cashpoint(); // initialise accounts int log_on(int id, int pin); // user logs on and gives // account number and PIN int log_off(); // user logs off int withdraw(Money amount); // withdraw some money int balance(Money &amount); // put balance of account into // "amount" and return status // NOTE: pass by reference!! }; #endif
The interface functions to the cashpoint allow the user to log on to a specific account (checking their PIN in the process), to withdraw money, obtain a balance of account and then log off.
Any of these could produce an error (e.g. trying to withdraw money before logging on to an account). The handling of these errors needs careful thought. A first reaction might be to print out error messages on the standard error channel from within the methods. This is a BAD idea because the Account class has no conception of what the user interface to the Cashpoint looks like (is it a dumb terminal or a PC with windows?). Instead, it is much better if the Account methods return a status code so that the program (or object) that called them can decide what to do. As a result, the Account class definition also defines the status codes that can be returned by its methods. These take the form of constant named integer values. This style of error handling is generally good practice.
Notice that becuase its result is a status code, the balance method actually passes the balance value back through the reference parameter Money &amount. This is an example of "pass by reference". In this case, we are using pass by reference so that we can have the deliberate side-effect of passing a second value back out of the function. Because we want a side-effect we mustn't use the word const in the declaration of this paramter.
The implementation of the Cashpoint class is included below.
// Implementation of the Cashpoint class // Steve benford - November 1991 #include "Cashpoint.h" #include <stream.h> // initialise accounts - not normally done in a cashpoint // but necessary for this example Cashpoint::Cashpoint() { int pin; // initial balance for accounts created // as 1000 pounds Money m(1000,0); for(int i = 0; i< MAX_ACCOUNTS; i++) { // set account number accounts[i].set_account_id(i); // allocate an initial PIN pin = accounts[i].allocate_pin(); cout << "Account id " << i << " has PIN " << pin << "\\n"; // give an initial balance accounts[i].credit(m); } // note that there is no account currently in use current = NO_ACCOUNT_OPEN; } // user logs on - find account and check PIN int Cashpoint::log_on(int id, int pin) { // make sure no other account is in use if (current != NO_ACCOUNT_OPEN) return BUSY; // look for an account matching the id for(int i = 0; i < MAX_ACCOUNTS; i++) if(accounts[i].get_account_id() == id) // if we found one, check the PIN if(accounts[i].check_pin(pin) == 1) { // if PIN matches, open account for access current = i; return SUCCESS; } else // PIN didn't match return AUTH_FAILURE; // if we get here then no account is open and the ID must be wrong return NO_SUCH_ACCOUNT; } // log off int Cashpoint::log_off() { // make sure that an account is open if(current == NO_ACCOUNT_OPEN) { return NO_ACCOUNT_OPEN; } else { // close the account current = NO_ACCOUNT_OPEN; return SUCCESS; } } // withdraw some money int Cashpoint::withdraw(Money amount) { // first check that an account is open if(current == NO_ACCOUNT_OPEN) return NO_ACCOUNT_OPEN; // withdraw the money if(accounts[current].debit(amount) == 1) // indicate that the withdrawal is OK return SUCCESS; else return INSUFFICIENT_FUNDS; } // return the balance of account AND indication of success/failure // the balance is assigned to the reference parameter "m" // the status is returned as the function result int Cashpoint::balance(Money &m) { // first check that an account is open if(current == NO_ACCOUNT_OPEN) // if not, return suitable error_code return NO_ACCOUNT_OPEN; else { // assign balance to parameter "m" and return SUCCESS m = accounts[current].balance(); return SUCCESS; } }
The implementation of the Cashpoint simulator accesses an array of Account objects declared as in internal data member.
Account accounts[MAX_ACCOUNTS]; // managed accounts
The constructor for Cashpoint has to initialise the objects in this array. Of course, in the real-world, the accounts would be set up by some other program, not by the cashpoint machine each time it was started up. Also notice the syntax for invoking a method on an object stored in this array:-
accounts[i].set_account_id(i);
This example says, "get the ith element of the accounts array (an object of class Account) and invoke the set_account_id method on it".
So far we have seen a Money class, an Account class which uses the Money class and a Cashpoint class which uses the Account and Money classes. To complete the program we need a "user interface" which communicates with the user and invokes the appropriate methods on a Cashpoint object. This is where the function main() will be defined.
// Interface to the Cashpoint object // Steve Benford - November 1991 #include "Cashpoint.h" #include <stream.h> // function to process menu transactions on a cashpoint object void do_transactions(Cashpoint &c); // function to convert error codes into error messages void print_error(int error); // Function to work out how many notes of which denominations // should be given out void give_money(Money m); main() { // set up cashpoint object Cashpoint cpt; int id, pin; // account identification int status; // return status of operations // log users on and off (infinite loop) while (1) { // prompt for and read the account id and pin cout << "please enter your account number: "; cin >> id; cout << "Please enter your PIN: "; cin >> pin; // try to log on status = cpt.log_on(id, pin); if(status == SUCCESS) { // do_transactions(); do_transactions(cpt); // log this person off cpt.log_off(); } else // print out a suitable error message print_error(status); } // while } // main // process the transactions for a specific session void do_transactions(Cashpoint &c) { // menu selection char ch; // return status of operations int status; Money amount(0,0); int n; while(1) { // show menu of options cout << "\\nw withdraw some money"; cout << "\\nb balance of account"; cout << "\\nq quit"; cout << "> "; cin >> ch; // process selection switch(ch) { case 'w': cout << "Enter amount (multiples of 5 only) >"; cin >> n; // check the user entered a multiple of 5 if(n % 5 != 0) { cerr << n << "not a multiple of 5\\n"; break; } // set the value of amount and withdraw it amount.set(n,0); status = c.withdraw(amount); if(status == SUCCESS) give_money(amount); else print_error(status); break; case 'b': // look up the balance status = c.balance(amount); if(status == SUCCESS) { cout << "your balance is currently "; amount.print(); cout << "\\n"; } else print_error(status); break; case 'q': cout << "returning\\n"; return; default: cerr << ch << " is not a valid selection\\n"; } // switch } // while } // do_transactions // give out money! // calculate the number of 50, 20, 10 and 5 pound notes needed to // make up the total amount given. void give_money(Money m) { int pnds = m.pounds(); cout << "here's your money ...\\n"; // note the use of integer divide (/) and modulo (%) operations // to obtain the quotient and remainder of divisions. cout << pnds << "\\n"; cout << pnds / 50 << " fifty pound notes\\n"; pnds = pnds % 50; cout << pnds / 20 << " twenty pound notes\\n"; pnds = pnds % 20; cout << pnds / 10 << " ten pound notes\\n"; pnds = pnds % 10; cout << pnds / 5 << " five pound notes\\n"; } // process error codes and print out messages void print_error(int error) { switch(error) { case SUCCESS: cout << "Operation completed successfully\\n"; break; case BUSY: cout << "Cashpoint busy with another account\\n"; break; case NO_ACCOUNT_OPEN: cout << "There is no account currently open\\n"; break; case NO_SUCH_ACCOUNT: cout << "There is no account with this ID\\n"; break; case AUTH_FAILURE: cout << "Invalid PIN\\n"; break; case INSUFFICIENT_FUNDS: cout << "There is not enough money in your account\\n"; break; default: cout << "Unknown error code generated by cashpoint!\\n"; } //switch } //print_error
The function main() sets up a Cashpoint object and then follows a continuous while loop, logging the user onto an account. When the user is logged on, the function do_transactions is called. This sits in a second continuous loop, presenting users with a menu of choices, reading their selection, processing it though a switch statement and invoking the appropriate methods on the cashpoint object. Logging out of the account returns to the outer while loop in the function main.
Two other useful functions are defined: give_money prints out a combination of 50, 20, 10 and 5 pounds notes which make up the amount withdrawn. Print_error converts status codes from the Cashpoint into error messages.
One feature of this design is that we could easily build alternative user interfaces to the same Cashpoint class.
The overall system has been divided up into a number of modules representing different C++ classes. Provided that the header (.h) files are well defined, each implementation could be written independently by a different member of a programming team.
The following commands would be needed to compile the various components into a final executable program.
Compile the Money implementation:
Compile the Account implementation:
Compile the Cashpoint implementation:
Compile the interface and link in all the other compiled code:
Note that the order of the .o files in the last command is significant!
Notes converted from troff to HTML by an Eric Foxley shell script, email errors to me!