Tải bản đầy đủ - 0 (trang)
Chapter 13. Similar Classes: How to Treat Them

Chapter 13. Similar Classes: How to Treat Them

Tải bản đầy đủ - 0trang

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



In the first part of this book, you learned computational aspects of C++. You learned about such

traditional programming topics as data types, identifiers, keywords, expressions, statements,

conditional statements, loops, and other control constructs. The skills of using these tools are

necessary prerequisites to anything else you would like to do in any programming language, not

only in C++. These skills allow you to create code that accomplishes the program goal and

produces necessary results in terms of computational requirements.

Also in the first part of the book, you studied the methods of aggregation¡Xputting data

components together into arrays, structures, and other programmer-defined types, and putting

statements and control constructs together into functions. Doing this in C++ is more complex than

in other languages, especially when it comes to handling name scopes, passing parameters,

returning values, pointers, and references. You also became familiar with the joys and perils of

C++ dynamic memory management. These are the tools that are directed towards breaking the

program into cooperating parts; however, the tools are directed more toward the programmers'

convenience than toward achieving the computational goals of the program. The computational

goals could be achieved by a variety of design alternatives, but the quality of the program (from the

point of view of its maintainability) might be quite different.

The skill of combining C++ coding elements correctly and separating coding elements that should

not belong together is a necessary prerequisite for writing maintainable and modifiable C++

programs.

In the second part of the book, you studied how to apply what you learned in the first part of the

book to writing C++ classes. You studied class syntax, class scope, data members, member

functions, access to data and functions, messages with their syntax and meaning, object

initialization, different kinds of constructors and destructors, static data, and functions. You learned

about operator functions, which make C++ code so much nicer but which make the design of

classes so much more complex. You learned about friends. You also learned how to recognize

dangerous elements of class design and how to avoid their negative consequences for your

programs. Writing programs with classes makes C++ much, much more complex than other

languages, but it is worth the trouble.

The skill of putting together (into the same class) related data and functions is a necessary

prerequisite for writing object-oriented programs. The major difference between traditional and

object-oriented programs is that traditional C++ programs are built from cooperating global

functions that bind together the steps of each operation. In contrast, object-oriented programs are

built from cooperating classes that bind together data and operations over that data.

However, the first two parts of the book were only a prelude to object-oriented programming. In all

examples, you dealt with only one class because you were concentrating on the details of class

design rather than on relationships among classes. In the third part of the book, you began studying

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (737 of 1187) [8/17/2002 2:58:01 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



the building of C++ programs as sets of cooperating classes. This requires implementing

relationships among classes in a C++ program. In Chapter 12, "Composite Classes: Pitfalls and

Advantages," you saw how one object (server object) could be used as a component of another

object (its client object). The member functions of the component object provide services to the

member functions of the composite class. This is the most common simple relationship among

objects.

One object can also be pointed to by a pointer, which is a data member of the another object. The

client object gains access to the member functions of the server object by sending messages to its

pointer data member. One object can also be used as a reference data member of the client object.

Syntactically, this is similar to simple class composition, but actually this is a very different

relationship between the objects.

With simple class composition, the server object (a component) is a data member of the client

object (a composite object). In this relationship, the client object has an exclusive use of its

component object. When the server object is a reference (or a pointer) data member of the client

object, the server object might be shared between several client objects; several objects can point to

the same server object. The changes to the server object affect the state of the client object (or

several client objects). It does not make sense to discuss which relationship is "better" in general,

exclusive composition or sharing of components. For many practical situations, however, one

relationship is "better" than another in the sense that it better represents the relationship between

real-life entities modeled by the C++ program. It is important to choose the relationship that best

models the real-life objects.

We also looked at a very popular relationship between objects, when one object is implemented as

a container, and a set of objects of another class (rather than a single object) serves as a component

of this container. This relationship between objects is often found in C++ programs, and you should

feel comfortable arranging the objects in your application in their appropriate relationships.

In this chapter, we will continue the study of cooperation between the parts of C++ code. You will

be introduced to C++ inheritance as a mechanism to represent the relationship among classes of the

application. At this stage, the difference between the relationship between objects and the

relationship between classes might look vague to you. By the end of this chapter you will see the

difference.

Inheritance is used very often in C++ programs¡Xand rightly so. This is a powerful mechanism for

reusing C++ designs, for labor division among programmers, and for introducing modularity into

C++ programs. To use inheritance correctly, you should learn its syntax, methods of instantiation of

derived objects, techniques of access to components, rules for function call resolution, and much

more. It is also important to learn how to decide whether to use inheritance at all or whether class

composition will do the job better. For all the power and utility of inheritance, C++ programmers

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (738 of 1187) [8/17/2002 2:58:01 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



may sometimes overuse inheritance in situations where the use of inheritance results in creating

additional relationships and dependencies that make the program harder to understand.

See for yourself.



Treating Similar Classes

Our programs model a variety of real-life objects through their data (object state) and operations

(object behavior). This is a mantra of object-oriented design, but it is up to each designer to decide

what to include in each class. The modeling of real-life entities should ideally reflect "common

features" among real objects, for example, among inventory items, event counters, or bank

accounts.

These "common features" are of course in the eye of the beholder, and C++ has different

mechanisms for representing different degrees of similarity among entities.

The first mechanism that C++ offers for capturing the common features among real-life objects is

the class construct itself. We use the class construct to capture commonality of objects when we

believe that these objects can be characterized by the same sets of attributes and the same patterns

of behavior. These objects are different in values of state attributes: The corner points of the

different rectangles have different coordinates, different inventory items have different titles, and

each account has its own balance and its own account owner. The common elements are that each

rectangle has corner points, each item has a title, and each account has a balance and an account

owner. If one account needs the interest rate to be specified and another account does not, these two

accounts normally should not be viewed as objects of the same class.

Often, the situation is not clear cut. For example, each bolt in an inventory might have its own

individual characteristics that make it different from all other bolts in the application. You need to

design a separate class for each bolt object, give each class an individual set of data members and

member functions that describe each bolt, and create a set of unique names for each class. These

names might reflect the unique nature of each bolt in the application, for example, RustyBolt,

UglyBolt, and BoltFoundInPothole. This may be complicated and make sense only if different

bolts do not have common features and each bolt behaves differently.

However, if the bolts in the inventory have enough in common that you can represent each bolt

using the same names for data members as for data members of other bolts in the application, this

removes the need to represent each bolt as an object of a different class. You might get away with

using only one class, for example, Bolt, and represent each bolt in your application as an object of

that class, with such attributes as the date of purchase, the name of the vendor, and pitch. Similarly,

you can represent all nuts in the inventory as objects of the same class, Nut, if the same set of

attributes (color, material, size, etc.) sufficiently describes each nut object.

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (739 of 1187) [8/17/2002 2:58:01 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



If it turns out that class Bolt, class Nut, and other inventory items use the same names for their

data members, you might abandon the idea of distinguishing between nuts and bolts and use only

one class, InventoryItem, to represent these diverse objects. If all the bolts are the same from the

point of view of the application, you can represent them as a single object and specify the quantity

of bolts among the attributes of the class. Since all the bolts are the same, the differences in pitch

are not important. If pitch is important, this design cannot be used.

If all that is of interest to the application is the total cost of nuts and bolts and other inventory items,

we can represent inventory as an object of type Asset, with attributes appropriate for the goals of

the application.

Often, however, commonalities might exist among classes: The groups of objects might have

basically similar but still somewhat different sets of attributes and operations.

For example, small bolts might have their weight specified per 100 bolts, and large bolts might

have their weight specified per each bolt, and they might have an attribute for the maximum force

allowed to be applied to a large bolt.

Similar, hourly employees might have their pay rate hourly and the number of hours worked during

the week specified as data members. Salaried employees might have all the same attributes (name,

address, date of hire, etc.), but instead of pay per hour and number of hours worked, they might

have salary per year specified.

Some object groups might have somewhat different sets of operations or provide additional

operations. For example, savings accounts might pay interest, and checking accounts might charge

transaction fees. Simply merging all these characteristics into one class will satisfy the client code

requirements but is inherently unsafe. The client code might use the object incorrectly, assuming

the presence of the features that are there for other objects but that are not there for this particular

object. For example, the client code might try to pay interest on the checking account and charge

transaction fees on the savings account.

Still, merging all attributes and operations into one class to provide for all possible alternatives is a

viable method of abstraction. It is up to the client code to make sure that each object is used

according to its inherent characteristics.



Merging Subclass Features into One Class

As an example, let us consider something everyone is familiar with (or so I hope) either from firsthand experience or heard from others.

I will discuss a simplified class Account with a data member balance and member functions

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (740 of 1187) [8/17/2002 2:58:01 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



and deposit(). For a checking account, the withdrawal operation should impose a

charge (i.e., 20 cents). For a savings account, the daily interest is added (i.e., at the yearly rate of

6%). The values for charge and interest rate are represented as data members in the Account class.

For simplicity of the example, I am not discussing the techniques for specifying and changing the

numerical literals and other countless practical details such as the owner name, address, age, social

security number, overdraft fee, and other grim (and cheerful) details of the banking business.

withdraw()



Listing 13.1 shows a program that implements the properties of both savings and checking accounts

in the combined class Account. The client code defines Account objects and performs appropriate

operations. This kind of client code is typical of pre-object-oriented standards of programming,

which reflected our belief (often groundless) that humans always use variables correctly.



Example 13.1. Example of combining diverse features in the same class Account.

#include

using namespace std;

class Account {

double balance;

double rate;

double fee;



// for all kinds of accounts

// for savings account only

// for checking accounts only



public:

Account(double initBalance = 0)

{ balance = initBalance; fee = 0.2; }



// for checking accounts only

// use fee but not rate



Account (double initBalance, double initRate)

{ balance = initBalance; rate = initRate; }



// for savings

// no fee here



double getBal()

{ return balance; }



// common for both accounts



void withdraw(double amount)

{ if (balance > amount)

balance -= amount; }



// common for both accounts



void deposit(double amount)

{ balance += amount; }



// common for both accounts



void payInterest()

{ balance += balance * rate / 365 / 100; }

void applyFee()

{ balance -= fee; }

} ;



// for savings accounts only



// for checking accounts only



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (741 of 1187) [8/17/2002 2:58:01 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



int main()

{

Account a1(1000), a2(1000,6.0);

cout << "Initial balances: " << a1.getBal()

<< " " << a2.getBal() << endl;

a1.withdraw(100); a2.deposit(100);

a2.payInterest(); a1.applyFee();

cout << "Ending balances: " << a1.getBal()

<< " " << a2.getBal() << endl;

return 0;

}



// a1: checking, a2: savings



// no problem

// no errors



Today, we no longer believe in human infallibility. If something can be typed in, somebody,

someplace, sometime will type it in. For example, the fifth line of the client code above could have

been written this way:

a1.payInterest();



a2.applyFee();



// miss takes a maid (joke)



You cannot, of course, prevent all coding mistakes (this is why testing is needed), but you should

prevent as many as possible. Or at least make sure that you are notified of errors without the need

to compute the actual output. This design needs to be improved.

Notice that the client code makes an explicit comment about the nature of each account when the

account is created, but nothing in this design would allow the client programmer to express this

idea in code rather than comments. For this to become possible, the server class (in this case,

Account) should support the needs of the client by assuming the responsibility to explicitly

distinguish between different kinds of Account objects.



Pushing Responsibility for Program Integrity to the Server

To avoid the danger of incorrect use of a server object by the client code, you can add to the server

class an additional attribute, a tag field, which describes what kind of account this particular object

is. This means that you are introducing subclasses to the class.

When an object is created, this tag field could be set to indicate the object subclass during the

object initialization. When the object is used (e.g., payInterest() or applyFee()), this field is

checked to make sure that the operation is legal for this kind of object.

For example, when an Account object is created, I could set the tag field to zero if the object were

going to be used as a checking account. If the object were going to be used as a savings account, I

would set the tag field to one. This means that the constructor should somehow know what kind of

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (742 of 1187) [8/17/2002 2:58:01 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



Account



object is being created.



In this example, I can make clever use of the fact that the constructors for two different kinds of

accounts have a different number of parameters. Also, using numeric values for the tag field is not

good software engineering practice. The designer of the code knows that zero means checking and

one means savings. All others run the risk of being confused. How can the designer pass the

knowledge (in this case, which tag value means checking and which tag value means savings) to

the maintainer? This is what enumeration types are used for in C++. I make a field of the

enumeration type Kind local to class Account. Because the type Kind is not going to be used

outside of class Account, I nest the enumeration type Kind within class Account. This name does

not pollute the global name space and does not prevent someone else on the project from using this

name elsewhere.

class Account {

enum Kind { CHECKING, SAVINGS } ;

double balance;

double rate, fee;

Kind tag;

public:

Account(double initBalance = 0)

{ balance = initBalance; fee = 0.2;

tag = CHECKING; }

Account (double initBalance, double initRate)

{ balance = initBalance; rate = initRate;

tag = SAVINGS; }

. . . } ;



// constants for account kind



// tag field for object kind

// checking account



// savings account



// the rest of Account class



If it were not for this stroke of luck, the type Kind would be made available to the client code as

well, and the client code would explicitly specify what kind of account is being created. This means

that the constructor code would include a parameter for the account kind.

Let me make the example more difficult (and realistic) by assuming that the initial interest rate is

the same for all savings accounts (of this kind) and hence does not have to be specified by the client

code. Because of this, class Account needs only one constructor. Because of this, the client code

has to specify the kind of account object. Because of this, the type Kind should be made global (and

pollute the global name space, thus increasing the need for cooperation among team members).

Here is how this new class Account looks:

enum Kind { CHECKING, SAVINGS } ;



// constants for account kind



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (743 of 1187) [8/17/2002 2:58:01 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



class Account {

double balance;

double rate, fee;

Kind tag;

public:

Account(double initBalance, Kind kind)

{ balance = initBalance; tag = kind;

if (tag == CHECKING)

fee = 0.2;

else if (tag == SAVINGS)

rate = 6.0; }

. . . } ;



// tag field for object kind

// one constructor only

// set the tag field

// it is checking account

// it is savings account

// the rest of Account class



Notice that I resist the temptation to use the same memory location for the interest rate if this is a

savings account object and for the check cashing fee if this is a checking account object. If the

application had to handle a large number of Account objects in memory and memory were at a

premium, this could be considered too. Otherwise, it would just introduce additional dependencies

to the code. Avoid alternative uses of memory.

Now the client code explicitly uses the enumeration values to specify what kind of Account object

is being constructed. Notice that comments became redundant¡Xthey would only repeat what the

code designer has now expressed in code so that the designer's knowledge is transmitted to the

maintainer.

Account a1(1000,CHECKING);

Account a2(1000,SAVINGS);



// a1 is checking account

// a2 is savings account



Polluting of the name space by the enumeration type Kind can be avoided even when the client

code needs to use the values of this type as in the example above. One way to achieve this is to

make the type local in class Account again.

class Account {

double balance;

double rate, fee;

Kind tag;

public:

enum Kind { CHECKING, SAVINGS };

Account(double initBalance, Kind kind)

{ balance = initBalance; tag = kind;

if (tag == CHECKING)



// tag field for object kind

// constants for account kind

// one constructor only

// set the tag field



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (744 of 1187) [8/17/2002 2:58:01 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



fee = 0.2;

else if (tag == SAVINGS)

rate = 6.0; }

. . . } ;



// it is checking account

// it is savings account

// the rest of Account class



The client code now has to use the class scope operator when using the enumeration literal values

as constructor arguments.

Account a1(1000,Account::Kind::CHECKING);

Account a2(1000,Account::Kind::SAVINGS);



// a1 is checking account

// a2 is savings account



For this design to fly, the type Kind cannot be defined in the private part of class Account as I did

in the first example of the Account class with two constructors. The type has to be public for its

literals to be accessible in the client code. Notice that the use of this type inside the class Account

(for data member tag) does not have to follow the definition of the type. Although C++ compilers

are one-pass compilers, they give you a break by making two passes inside the class definition.

This is true, however, only for newer compilers. Some older compilers might give you a hard time

by complaining that type Kind in the definition of the field tag is not defined. For these compilers,

the definition of the type Kind should precede the definition of the field tag. To make this

definition visible in the client code, it has to be placed in the public part of the class definition. To

reconcile these contradicting requirements, you can have additional public and private sections in

the class definition.

class Account {

double balance;

double rate, fee;

public:

enum Kind { CHECKING, SAVINGS };

private:

Kind tag;

public:

Account(double initBalance, Kind kind)

{ balance = initBalance; tag = kind;

if (tag == CHECKING)

fee = 0.2;

else if (tag == SAVINGS)

rate = 6.0; }

. . . } ;



// constants for account kind

// tag field for object kind

// one constructor only

// set the tag field

// it is checking account

// it is savings account

// the rest of Account class



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (745 of 1187) [8/17/2002 2:58:01 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



With the object tag field properly initialized in the constructor, the designer of class Account can

now protect the client code from its own inconsistencies. To make sure the client programmer does

not erroneously charge the fee after a call to withdraw() for a savings account object, the server

class (class Account) checks the nature of the object and applies the fee to a checking account only.

void withdraw(double amount)

{ if (balance > amount)

{ balance -= amount;

if (tag == CHECKING)

only

balance -= fee; } }



// common for both accounts



// for checking accounts



As you can see, the functionality of applyFee() is now provided by the member function

withdraw() so that the client programmer does not have to remember for what kind of object it has

to be called. I hope that you recognize the concepts of information hiding and pushing

responsibility down to the server at work here.

Similarly, the method payInterest() checks whether the object that is the target of the message is

a savings account. If it is, the interest for the day is paid. If the account is a checking account, a runtime error message is printed notifying the tester that the client programmer made a mistake calling

this function on a wrong object, and the operation is aborted.

Notice the terminology. It is the designer of the Account class who does the work on behalf of the

client code. In pre-object-oriented programming days, the client code had to protect itself (or make

sure there were no errors). In object-oriented programming days, we push responsibility from the

client code to the server class. This is a very common design approach. Make sure you feel

comfortable using it.

Listing 13.2 shows the implementation of class Account that applies this technique to the

validation of client actions. Notice that the Kind type is defined outside of class Account. The

output of the program run is shown in Figure 13-1.



Figure 13-1. Output for program in Listing 13.2.



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (746 of 1187) [8/17/2002 2:58:01 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



Example 13.2. Example of run-time test of correctness of client code.

#include

using namespace std;

enum Kind { CHECKING, SAVINGS } ;



// constants for account kind



class Account {

double balance;

double rate, fee;

Kind tag;



// tag field for object kind



public:

Account(double initBalance, Kind kind)

{ balance = initBalance; tag = kind;

if (tag == CHECKING)

fee = 0.2;

else if (tag == SAVINGS)

rate = 6.0; }

double getBal()

{ return balance; }

void withdraw(double amount)

{ if (balance > amount)

{ balance -= amount;

if (tag == CHECKING)

balance -= fee; } }



// set the tag field

// for checking account

// for savings account



// common for both accounts

// common for both accounts



// for checking accounts only



void deposit(double amount)

{ balance += amount; }

void payInterest()

// for savings account only

{ if (tag == SAVINGS)

balance += balance * rate / 365 / 100;

else if (tag == CHECKING)

cout << " Checking account: illegal operation\n"; }

} ;

int main()

{

Account a1(1000,CHECKING);

Account a2(1000,SAVINGS);



// a1 is checking account

// a2 is savings account



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (747 of 1187) [8/17/2002 2:58:01 PM]



Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Chapter 13. Similar Classes: How to Treat Them

Tải bản đầy đủ ngay(0 tr)

×