13.10. (Optional) Software Engineering
Case Study: Incorporating Inheritance into the ATM System
We now revisit our ATM system design to
see how it might benefit from inheritance. To apply inheritance, we first look
for commonality among classes in the system. We create an inheritance hierarchy
to model similar (yet not identical) classes in a more efficient and elegant
manner that enables us to process objects of these classes polymorphically. We
then modify our class diagram to incorporate the new inheritance relationships.
Finally, we demonstrate how our updated design is translated into C++ header
files.
In Section
3.11, we encountered the problem of representing a
financial transaction in the system. Rather than create one class to represent
all transaction types, we decided to create three individual transaction
classes—BalanceInquiry, Withdrawal and Deposit—to represent the transactions that the ATM system can
perform. Figure
13.26 shows the attributes and operations of
these classes. Note that they have one attribute (accountNumber) and
one operation (execute) in common.
Each class requires attribute accountNumber to specify the account to which the transaction
applies. Each class contains operation execute, which the ATM
invokes to perform the transaction. Clearly, BalanceInquiry,
Withdrawal and Deposit represent types
of transactions. Figure 13.26 reveals commonality among the transaction classes, so
using inheritance to factor out the common features seems appropriate for
designing these classes. We place the common functionality in base class
Transaction and derive classes BalanceInquiry, Withdrawal and
Deposit from Transaction (Fig. 13.27).


The UML specifies a relationship called a generalization to model inheritance. Figure 13.27 is the class diagram that models the inheritance
relationship between base class Transaction and its three derived classes. The
arrows with triangular hollow arrowheads indicate that classes
BalanceInquiry, Withdrawal and Deposit are derived from class Transaction. Class
Transaction is said to be a generalization of its
derived classes. The derived classes are said to be specializations of class
Transaction.
Classes BalanceInquiry, Withdrawal and
Deposit share integer attribute
accountNumber, so we factor out this common
attribute and place it in base class Transaction. We no longer list
accountNumber in the second compartment
of each derived class, because the three derived classes inherit this attribute
from Transaction. Recall, however, that derived classes cannot access
private attributes of a base class. We therefore include
public member function getAccountNumber in class
Transaction. Each derived class inherits this
member function, enabling the derived class to access its
accountNumber as needed to execute a
transaction.
According to Fig. 13.26, classes
BalanceInquiry, Withdrawal and Deposit also share operation
execute, so base class Transaction should contain
public member function execute. However, it does not make
sense to implement execute in class Transaction, because the functionality that this member function
provides depends on the specific type of the actual transaction. We therefore
declare member function execute as a pure virtual function in base class
Transaction. This makes Transaction an abstract class and forces any class derived from
Transaction that must be a concrete class
(i.e., BalanceInquiry, Withdrawal and Deposit) to implement pure virtual member function
execute to make the derived class concrete.
The UML requires that we place abstract class names (and pure virtual
functions—abstract operations in the UML)
in italics, so Transaction and its member function execute appear in
italics in Fig.
13.27. Note that operation execute is not italicized in derived classes
BalanceInquiry, Withdrawal and Deposit.
Each derived class overrides base class Transaction's execute member function
with an appropriate implementation. Note that Fig. 13.27
includes operation execute in the third compartment of classes
BalanceInquiry, Withdrawal and Deposit, because each class has a different concrete
implementation of the overridden member function.
As you
learned in this chapter, a derived class can inherit interface or implementation
from a base class. Compared to a hierarchy designed for implementation
inheritance, one designed for interface inheritance tends to have its
functionality lower in the hierarchy—a base class signifies one or more
functions that should be defined by each class in the hierarchy, but the
individual derived classes provide their own implementations of the function(s).
The inheritance hierarchy designed for the ATM system takes advantage of this
type of inheritance, which provides the ATM with
an elegant way to execute all transactions "in the general." Each class derived
from Transaction inherits some implementation
details (e.g., data member accountNumber), but
the primary benefit of incorporating inheritance into our system is that the
derived classes share a common interface (e.g., pure virtual member
function execute). The ATM can aim a Transaction
pointer at any transaction, and when the ATM invokes execute through this pointer, the version of execute
appropriate to that transaction (i.e., the version implemented in that derived
class's .cpp file) runs automatically. For
example, suppose a user chooses to perform a balance inquiry. The ATM aims a
Transaction pointer at a new object of class BalanceInquiry;
the compiler allows this because a BalanceInquiry is a Transaction.
When the ATM uses this pointer to invoke execute, BalanceInquiry's
version of execute is called.
This polymorphic approach also makes
the system easily extensible. Should we wish to create a new transaction type
(e.g., funds transfer or bill payment), we would just create an additional
Transaction derived class that overrides the execute member function with a version appropriate for the new
transaction type. We would need to make only minimal changes to the system code
to allow users to choose the new transaction type from the main menu and for the
ATM to instantiate and execute objects of the new derived class. The ATM could
execute transactions of the new type using the current code, because it executes
all transactions identically.
As you learned earlier in the chapter, an
abstract class like Transaction is one for
which you never intend to instantiate objects. An abstract class simply declares
common attributes and behaviors for its derived classes in an inheritance
hierarchy. Class Transaction defines the concept
of what it means to be a transaction that has an account number and executes.
You may wonder why we bother to include pure virtual member function
execute in class Transaction if
execute lacks a concrete implementation.
Conceptually, we include this member function because it is the defining
behavior of all transactions—executing. Technically, we must include member
function execute in base class Transaction so that the ATM (or any other class) can
polymorphically invoke each derived class's overridden version of this function
through a Transaction pointer or reference.
Derived classes BalanceInquiry, Withdrawal and
Deposit inherit attribute accountNumber from base class
Transaction, but classes Withdrawal and Deposit contain the
additional attribute amount that distinguishes them from class
BalanceInquiry. Classes Withdrawal and Deposit require this additional attribute to store the amount of
money that the user wishes to withdraw or deposit. Class
BalanceInquiry has no need for such an attribute
and requires only an account number to execute. Even though two of the three
Transaction derived classes share this
attribute, we do not place it in base class Transaction—we place only features common to all the derived classes in the
base class, so derived classes do not inherit unnecessary attributes (and
operations).
Figure
13.28 presents an updated class diagram of our
model that incorporates inheritance and introduces class Transaction. We model an association between class ATM and class Transaction to show that
the ATM, at any given moment, either is executing a transaction or is not (i.e.,
zero or one objects of type Transaction exist in the system at a time).
Because a Withdrawal is a type of Transaction, we no longer draw an association line directly between
class ATM and class Withdrawal—derived class Withdrawal inherits base
class Transaction's association with class ATM. Derived classes
BalanceInquiry and Deposit also inherit
this association, which replaces the previously omitted associations between
classes BalanceInquiry and Deposit and class
ATM. Note again the use of triangular hollow arrowheads to indicate the
specializations of class Transaction, as indicated in Fig. 13.27.
We also add an association between
class Transaction and the BankDatabase (Fig. 13.28). All
Transactions require a reference to the
BankDatabase so they can access and modify
account information. Each Transaction
derived class inherits this reference, so we no longer model the association
between class Withdrawal and the BankDatabase. Note that the
association between class Transaction and the BankDatabase
replaces the previously omitted associations between classes
BalanceInquiry and Deposit and the BankDatabase.
We include an association between class Transaction
and the Screen because all Transactions display output to the user via the Screen. Each
derived class inherits this association. Therefore, we no longer include the
association previously modeled between Withdrawal and the Screen. Class
Withdrawal still participates in associations with the
CashDispenser and the Keypad. We do
not move these associations to base class Transaction, because the association with the
Keypad applies only to classes Withdrawal and
Deposit, and the association with the CashDispenser applies only to class
Withdrawal.
Our class diagram incorporating
inheritance (Fig.
13.28) also models Deposit and BalanceInquiry. We show associations between Deposit and both the
DepositSlot and the Keypad. Note that class
BalanceInquiry takes part in no
associations other than those inherited from class Transaction—a
BalanceInquiry interacts only with the
BankDatabase and the Screen.
The class diagram of Fig.
9.20 showed attributes and operations with
visibility markers. Now we present a modified class diagram in Fig. 13.29 that includes abstract
base class Transaction. This abbreviated
diagram does not show inheritance relationships (these appear in Fig. 13.28), but instead shows the attributes and operations
after we have employed inheritance in our system. Note that abstract class name
Transaction and abstract operation name execute in class
Transaction appear in italics. To save space, as we did in Fig.
4.20, we do not include those attributes shown by associations in Fig. 13.28—we do,
however, include them in the C++ implementation in Appendix
E. We also omit all operation parameters, as we did in Fig.
9.20—incorporating inheritance does not affect the parameters already
modeled in Figs.
6.34–6.37.

Software Engineering Observation 13.12
|
A complete class
diagram shows all the associations among classes and all the attributes and
operations for each class. When the number of class attributes, operations and
associations is substantial (as in Fig. 13.28 and Fig. 13.29), a good practice that promotes readability is to
divide this information between two class diagrams—one focusing on associations
and the other on attributes and operations. However, when examining classes
modeled in this fashion, it is crucial to consider both class diagrams to get a
complete view of the classes. For example, one must refer to Fig. 13.28 to observe the inheritance relationship
between Transaction and its derived
classes that is omitted from Fig.
13.29. |
Implementing the ATM System Design
Incorporating Inheritance
In Section
9.11, we began implementing the ATM system design in
C++ code. We now modify our implementation to incorporate inheritance, using
class Withdrawal as an example.
-
If a class A is a generalization of class B,
then class B is derived from (and is a specialization of) class
A. For example, abstract base class Transaction is a
generalization of class Withdrawal. Thus, class Withdrawal is derived
from (and is a specialization of) class Transaction. Figure 13.30 contains a portion of
class Withdrawal's header file, in which the class
definition indicates the inheritance relationship between Withdrawal
and Transaction (line 9).
Fig. 13.30. Withdrawal class definition that
derives from Transaction.
1 // Fig. 13.30: Withdrawal.h
2 // Definition of class Withdrawal that represents a withdrawal transaction
3 #ifndef WITHDRAWAL_H
4 #define WITHDRAWAL_H
5
6 #include "Transaction.h" // Transaction class definition
7
8 // class Withdrawal derives from base class Transaction
9 class Withdrawal : public Transaction
10 {
11 }; // end class Withdrawal
12
13 #endif // WITHDRAWAL_H
|
-
If class A is an abstract class and
class B is derived from class A, then class B must implement the pure virtual
functions of class A if class B is to be a concrete class. For example, class
Transaction contains pure virtual function
execute, so class Withdrawal must implement this member
function if we want to instantiate a Withdrawal object. Figure 13.31 contains the C++ header file for class Withdrawal from
Fig. 13.28 and Fig. 13.29. Class
Withdrawal inherits data member
accountNumber from base class Transaction, so Withdrawal does not declare this data member. Class
Withdrawal also inherits references to the
Screen and the BankDatabase from its base class Transaction, so we do not include these references in our code. Figure 13.29 specifies
attribute amount and operation execute for class Withdrawal. Line 19 of Fig. 13.31 declares a data member for attribute amount. Line 16
contains the function prototype for operation execute. Recall that, to be a
concrete class, derived class Withdrawal
must provide a concrete implementation of the pure virtual function execute in
base class Transaction. The prototype in
line 16 signals your intent to override the base class pure virtual function.
You must provide this prototype if you will provide an implementation in the
.cpp file. We present this implementation in Appendix
E. The keypad and cashDispenser references (lines 20–21)
are data members derived from Withdrawal's associations in Fig. 13.28. In the implementation of
this class in Appendix
E, a constructor initializes these references to
actual objects. Once again, to be able to compile the declarations of the
references in lines 20–21, we include the forward declarations in lines
8–9.
Fig. 13.31. Withdrawal class header file based on Fig. 13.28 and Fig. 13.29.
1 // Fig. 13.31: Withdrawal.h
2 // Definition of class Withdrawal that represents a withdrawal transaction
3 #ifndef WITHDRAWAL_H
4 #define WITHDRAWAL_H
5
6 #include "Transaction.h" // Transaction class definition
7
8 class Keypad; // forward declaration of class Keypad
9 class CashDispenser; // forward declaration of class CashDispenser
10
11 // class Withdrawal derives from base class Transaction
12 class Withdrawal : public Transaction
13 {
14 public:
15 // member function overriding execute in base class Transaction
16 virtual void execute(); // perform the transaction
17 private:
18 // attributes
19 double amount; // amount to withdraw
20 Keypad &keypad; // reference to ATM's keypad
21 CashDispenser &cashDispenser; // reference to ATM's cash dispenser
22 }; // end class Withdrawal
23
24 #endif // WITHDRAWAL_H
|
ATM Case Study Wrap-Up
This concludes
our object-oriented design of the ATM system. A complete C++ implementation of
the ATM system in 877 lines of code appears in Appendix
E. This working implementation uses key programming
notions, including classes, objects, encapsulation, visibility, composition,
inheritance and polymorphism. The code is abundantly commented and conforms to
the coding practices you've learned. Mastering this code is a wonderful capstone
experience for you after studying Chapters
1–13.
Software Engineering Case Study
Self-Review Exercises
| 13.1 |
The UML uses an
arrow with a___________to indicate a generalization relationship.
-
-
triangular hollow arrowhead
-
diamond-shaped hollow arrowhead
-
|
| 13.2 |
State whether the following
statement is true or false, and if false, explain why: The UML requires that we underline
abstract class names and operation names. |
| 13.3 |
Write a C++ header file to begin
implementing the design for class Transaction specified in Fig. 13.28 and Fig. 13.29. Be sure to include private references based on
class Transaction's associations. Also be sure to include
public get functions for any of the
private data members that the derived classes
must access to perform their tasks. |
Answers to Software Engineering
Case Study Self-Review Exercises
| 13.1 |
b. |
| 13.2 |
False. The UML requires that we
italicize abstract class names and operation names. |
| 13.3 |
The design for class Transaction yields the header file in Fig. 13.32. In the implementation in Appendix
E, a constructor initializes private reference attributes
screen and bankDatabase to
actual objects, and member functions getScreen and
getBankDatabase access these attributes. These member functions allow
classes derived from Transaction to access the ATM's screen and
interact with the bank's database.
Fig. 13.32. Transaction class header file based
on Fig. 13.28 and Fig.
13.29.
1 // Fig. 13.32: Transaction.h
2 // Transaction abstract base class definition.
3 #ifndef TRANSACTION_H
4 #define TRANSACTION_H
5
6 class Screen; // forward declaration of class Screen
7 class BankDatabase; // forward declaration of class BankDatabase
8
9 class Transaction
10 {
11 public:
12 int getAccountNumber(); // return account number
13 Screen &getScreen(); // return reference to screen
14 BankDatabase &getBankDatabase(); // return reference to bank database
15
16 // pure virtual function to perform the transaction
17 virtual void execute() = 0; // overridden in derived classes
18 private:
19 int accountNumber; // indicates account involved
20 Screen &screen; // reference to the screen of the ATM
21 BankDatabase &bankDatabase; // reference to the account info database
22 }; // end class Transaction
23
24 #endif // TRANSACTION_H
|
|