[Page 736 (continued)]|
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 classesBalanceInquiry, Withdrawal and Depositto 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).
Figure 13.26. Attributes and operations of classes BalanceInquiry, Withdrawal and Deposit.
Figure 13.27. Class diagram modeling generalization relationship between base class TRansaction and derived classes BalanceInquiry, Withdrawal and Deposit.
(This item is displayed on page 737 in the print version)
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 functionsabstract 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 hierarchya 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., 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, which the C++ compiler allows 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 the programmer never intends 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 transactionsexecuting. 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 transactionwe 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 Withdrawalderived 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.
Figure 13.28. Class diagram of the ATM system (incorporating inheritance). Note that abstract class name transaction appears in italics.
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, howeverthese associations apply to derived class Withdrawal but not to derived classes BalanceInquiry and Deposit, so we do not move these associations to base class transaction.
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 transactiona 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.24, we do not include those attributes shown by associations in Fig. 13.28we do, however, include them in the C++ implementation in Appendix G. We also omit all operation parameters, as we did in Fig. 9.20incorporating inheritance does not affect the parameters already modeled in Figs. 6.226.25.
Figure 13.29. Class diagram after incorporating inheritance into the system.
(This item is displayed on page 740 in the print version)
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 diagramsone 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.12, 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).
Figure 13.30. Withdrawal class definition that derives from TRansaction.
(This item is displayed on page 742 in the print version)
1 // Fig. 13.30: Withdrawal.h
2 // Definition of class Withdrawal that represents a withdrawal transaction
3 #ifndef WITHDRAWAL_H
4 #define WITHDRAWAL_H
6 #include "Transaction.h" // Transaction class definition
8 // class Withdrawal derives from base class Transaction
9 class Withdrawal : public Transaction
11 }; // end class Withdrawal
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 G. The keypad and cashDispenser references (lines 2021) are data members derived from Withdrawal's associations in Fig. 13.28. In the implementation of this class in Appendix G, a constructor initializes these references to actual objects. Once again, to be able to compile the declarations of the references in lines 2021, we include the forward declarations in lines 89.
Figure 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
6 #include "Transaction.h" // Transaction class definition
8 class Keypad; // forward declaration of class Keypad
9 class CashDispenser; // forward declaration of class CashDispenser
11 // class Withdrawal derives from base class Transaction
12 class Withdrawal : public Transaction
15 // member function overriding execute in base class Transaction
16 virtual void execute(); // perform the transaction
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
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 G. 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 1213.
Software Engineering Case Study Self-Review Exercises
The UML uses an arrow with a ________ to indicate a generalization relationship.
triangular hollow arrowhead
diamond-shaped hollow arrowhead
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.
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
False. The UML requires that we italicize abstract class names and operation names.
The design for class transaction yields the header file in Fig. 13.32. In the implementation in Appendix G, 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.
Figure 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
6 class Screen; // forward declaration of class Screen
7 class BankDatabase; // forward declaration of class BankDatabase
9 class Transaction
12 int getAccountNumber(); // return account number
13 Screen &getScreen(); // return reference to screen
14 BankDatabase &getBankDatabase(); // return reference to bank database
16 // pure virtual function to perform the transaction
17 virtual void execute() = 0; // overridden in derived classes
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
24 #endif // TRANSACTION_H