Tải bản đầy đủ - 0 (trang)
Chapter 12. Composite Classes: Pitfalls and Advantages

Chapter 12. Composite Classes: Pitfalls and Advantages

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

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



ϒΠ



Data Members with Special Properties



ϒΠ



Container Classes



ϒΠ



Summary



In the first two parts of this book, I mostly concentrated on the rules of the C++ language. I

discussed what you can and cannot do and what dangers you should be aware of to avoid loss of

performance or loss of program integrity. In these parts of the book, C++ emerges as a powerful

language that expects from the programmer a thorough understanding of what is happening on the

surface and under the hood of the program.

The second part of the book presented basic principles of object-oriented programming related to

building C++ code and to analyzing the interactions between program classes. Ideas were

introduced such as:

ϒΠ



binding data and functions in the class to indicate that they logically belong together



ϒΠ



making private those class components (data and functions) whose access from outside

the class would make class clients dependent on low-level details of class design



ϒΠ



using class scope as an additional tool to eliminate name conflicts between elements of

different classes and conferring among class developers



ϒΠ



providing member functions to make direct access to server data field names from client

code unnecessary



ϒΠ



pushing responsibility from client code down to server classes and member functions



ϒΠ



writing client code in terms of calls to server methods so that the code would not

develop dependencies on server design



ϒΠ



providing constructors and destructors for proper object initialization, resource

management, and for further pushing responsibility to server classes



ϒΠ



passing the server designer's knowledge about server behavior to the maintainer and to

client programmers, for example, with const modifiers for data members, parameters, return

values, and methods.

These ideas form the basis for programming techniques that result in self-documented objectoriented code. This code is easier to understand and to maintain. Only with the use of these ideas

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



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



can you realize the full potential of C++. Without them, your code will contain highly intertwined

parts that depend on each other with a large number of dependencies. Such code is difficult to

understand and to modify whether it is written in C++, Java, COBOL, or FORTRAN.

In this part of the book, we will switch from considering a stand-alone C++ class to the design of

programs that have several cooperating classes. You will study class composition, where objects of

one class are used as data members, local variables, or parameters for methods of another class.

This is a powerful technique of organizing cooperation among program classes. The design

decisions that you implement using class composition are supported by C++ rules for constructor

invocation and by C++ syntax for passing data from client code to the components of programmerdefined classes.

Another method of class cooperation is using inheritance, the method for designing classes that are

similar to each other, so that one class adds to data members and methods of another class. This is a

major vehicle for code reuse in C++. In addition to the design issues of deciding when inheritance

is appropriate, we will discuss all the relevant sets of C++ language features that support

inheritance: inheritance syntax, instantiation of objects, passing data for initialization of inherited

data members, name ambiguity, and rules for resolving ambiguity.

C++ programmers love to use inheritance. Many experts say that the use of inheritance is the

backbone of object-oriented programming. This is not really true. It is using C++ classes that is the

backbone of object-oriented programming¡Xbinding together data and operations, controlling

access to class components, and so on.

Inheritance is not the backbone of object-oriented programming. It is a technique for code and

design reuse. As such, it is very important for C++ programming. Let us make sure that this

important technique is used correctly.



Using Class Objects as Data Members

As was explained in Chapter 9, "C++ Class as a Unit of Modularization," the main purpose of the

class construct in C++ is to let the programmer bind together those data and operations that

logically belong together in the eyes of the class designer.

Almost all examples of C++ classes that appeared earlier in the book had data members of built-in

types¡Xintegers, and floating point numbers. Some more complex examples had a character array

as a data member. Actually, it was a pointer to a character array allocated on the heap. From the

point of view of class composition, a pointer is similar to integers and floating point numbers. It has

no internal structure of its own that is accessible from the outside.

However, you should not view this as an inherent limitation on class design. Class members can be

more complex than values of built-in types. Rather, it is an indication of a methodical approach to

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



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



the study of the language: starting from simpler features and progressing to more complex ones.

In Chapter 10,"Operator Functions: Another Good Idea," and Chapter 11, "Constructors and

Destructors: Potential Trouble," you saw how much attention C++ pays to treating built-in types

and programmer-defined types equally. If data members of built-in types can be used as class

components, there is no good reason why a data member of a class cannot be an object of some

other class that has components of its own.

C++ allows you to use class objects as components of objects of other classes. If one class has

many data members, you can merge a group of related data members into a larger object, and

declare this object to be a member of the class. Instead of a small number of large classes with

many components, you wind up with a larger number of classes with fewer components. This

facilitates the division of labor between programmers during development. Using a larger number

of smaller classes also improves modularization of code and facilitates hiding unnecessary details

from client code. The downside of overmodularization is that your clients can wind up with a large

number of small classes, which will make learning these classes more difficult.

Classes that have objects of other classes as their data members are called composite classes.

Almost all classes have components (data members) and hence are composite, but the term is used

mostly for classes whose components have their own components. In the theory of object-oriented

design, using objects of one class as components for objects of another class is called class

aggregation or class composition.

As an example, consider a class Rectangle that contains the x and y coordinates of its top-left and

bottom-right corners, respectively. This is a common convention in graphical programming.

class Rectangle {

int x1, y1;

// coordinates of the top-left

point

int x2, y2;

// coordinates of the bottom-right

point

int thickness;

// thickness of the rectangle

border

public:

Rectangle (int inX1, int inY1, int inX2, int inY2, int width=1);

void move(int a, int b);

// move rectangle

void setThickness(int width = 1);

// change thickness

bool pointIn(int x, int y) const;

// point in rectangle?

. . . . } ;

// the rest of class Rectangle

Rectangle::Rectangle (int inX1, int inY1, int inX2, int inY2, int width)

{ x1 = inX1; y1 = inY1;

x2 = inX2; y2 = inY2;

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



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



thickness = width; }



// set data members



void Rectangle::move(int a, int b)

{ x1 += a; y1 += b;

x2 += a; y2 += b; }



// move each corner



void Rectangle::setThickness(int width)

{ thickness = width; }



// do the job



bool Rectangle::pointIn(int x, int y) const // is

{ bool xIsBetweenBorders = (x1
bool yIsBetweenBorders = (y>y1 && y
return (xIsBetweenBorders && yIsBetweenBorders);



point in?

&& x
&& y>y2);

}



This class provides its clients with such services as moving the rectangle object around the screen,

changing the thickness of the lines used to draw the rectangle on the screen, and checking whether

a given point is inside the rectangle (hit test). The client code can define objects of class Rectangle

by specifying the coordinates of their corners. It moves a point and the rectangle around the screen,

trying to catch the point with the rectangle.

int x1=20,y1=40; int x2=70,y2=90;

// top-left/bottom-right corners

int x=100, y=120;

// point to catch by the rectangle

Rectangle rec(x1,y1,x2,y2,4);

// create a Rectangle object

rec.setThickness();

// line width is 1 pixel (default)

x -= 25; y -= 15;

// move the point around the screen

rec.move(10,20);

// 10 pixels to right, 20 pixels down

if (rec.pointIn(x,y)) cout << "Point is in\n";

// in point in

rectangle?



Even for this small example, I felt that the internal structure of the Rectangle class was too

complex. When I was writing this code, I made errors, confusing x1 and y1, x1 and y2, and so on.

As a client programmer, I felt it was too much work to specify the Rectangle object (five values in

the constructor call). The reason for this unnecessary complexity of class Rectangle and its client

is the lack of implementation for a component: class Point. Actually, the concept of point is

natural here, and it is even present in comments to both class Rectangle and its client, but this

concept is not supported by a programmer-defined type.



C++ Syntax for Class Composition

Let us consider the same example, this time using a class Point that provides the clients with a few

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



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



services. Similar to previous examples, I will ask you to imagine that this code is part of a huge

program with many people working on different parts. In this section, I will concentrate on the

syntax of class compositions and on issues related to communications among classes and among

people designing these classes.

class Point {

private:

int x, y;

public:

Point (int a, int b)

{ x = a; y = b; }

void set (int a, int b)

{ x = a; y = b; }

void move (int a, int b)

{ x += a; y += b; }

void get (int& a, int& b) const

{ a = x; b = y; }

bool isOrigin () const

{ return x == 0 && y == 0; } } ;



// private coordinates

// general constructor

// modifier function

// modifier function

// selector function

// predicate function



I am using here common terminology for member functions. A modifier is a member function that

changes the state of the target object. (See that it has no const keyword?) A selector is a member

function that does not change the state of the target object. (See that it has the const keyword?) A

predicate is a selector that returns a boolean value that tells something about the state of the target

object (in this case, whether it is at the point of origin).

In this example, I use generic names for member functions to illustrate the fact that class scope

effectively limits name conflicts in the program. When I choose the name set()for a Point

member function, I do not have to notify all team members who design other classes for the

application of my decision. They can use the name set() for their classes too. I will have to notify

only those few team members who design classes that use my class Point as a server to do their

job. One such client class is class Rectangle that I introduced at the beginning of this chapter. This

version of class Rectangle has two data members of class Point to denote the top-left and the

bottom-right corners of the rectangle. The data member thickness has the same meaning as

before¡Xit denotes the width of the line used to draw the rectangle on the screen.

class Rectangle {

Point pt1, pt2;

points

int thickness;



// top-left, bottom-right corner

// thickness of the rectangle



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



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



border

public:

Rectangle (int inX1, int inY1, int inX2, int inY2, int wid=1);

void move(int a, int b);

// move both points

void setThickness(int width = 1);

// change thickness

bool pointIn(int x, int y) const;

// point in rectangle

. . . . } ;

// the rest of class Rectangle

Rectangle::Rectangle (int inX1, int inY1, int inX2, int inY2, int width)

{ pt1.set(inX1,inY1); pt2.set(inX2,inY2); // push job down

thickness = width; }

// set data members

void Rectangle::move(int a, int b)

{ pt1.move(a,b); pt2.move(a,b); }



// pass buck to members



void Rectangle::setThickness(int width)

{ thickness = width; }



// do the job



bool Rectangle::pointIn(int x, int y) const // is point in?

{ int x1,y1,x2,y2;

// coordinates of corners

pt1.get(x1,y1); pt2.get(x2,y2);

// get point data

bool xIsBetweenBorders = (x1
bool yIsBetweenBorders = (y>y1 && yy2);

return (xIsBetweenBorders && yIsBetweenBorders); }



You see important changes in the design of class Rectangle. I wanted to say " significant

changes" but felt that it is not appropriate for such a small class. Whatever the term, these changes

represent common programming idioms for class composition.

In the Rectangle constructor, instead of a set of low-level assignments to numerous data members

of built-in types, there are two messages to component objects.

pt1.set(inX1,inY1); pt2.set(inX2,inY2);



// push job down



This is an example of insulating the client code (Rectangle class) from details of the design of the

server code (Point class). The client code here is written in terms of messages to server objects; the

client does not use low-level details of server design: The client code says what is being done

instead of spelling out the details of how it is being done. The responsibility for the details of the

operation is pushed from the Rectangle code to the Point code. This style of writing client code

makes it easier to understand.



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



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



The move() method represents an even more interesting C++ idiom for the relationship between

the composite and the component classes. When a Rectangle object is asked to move, the object

turns back and asks its components to do the work of calling the method with the same name,

move(). But this is not a recursive call to the same function; this second method move() belongs to

the component class Point, not to the composite class Rectangle. This is yet another example of

the tendency to treat objects of different natures equally in C++ code. In this case, the same

treatment means the methods with the same name that belong to classes with similar behavior.

Moving a rectangle means moving each of its points. The opportunity to write methods that pass

the buck to its data members is one of the reasons why both methods are called move() and not

movePoint() and moveRectangle().



Access to Data Members of Class Data Members

Another important difference between this design of class Rectangle and the previous version is

access to component's components. In the previous version, class Rectangle could do whatever it

wanted with coordinates x and y. They were directly accessible. In the last version of class

Rectangle, they are components of class Point. If the component object (in this example, of

class Point) had public components, the composite class (class Rectangle) could access the data

members of its object data member (in this example, x and y) using the dot selector operator. That

is, if Point components were public, the Rectangle member function Rectangle::pointIn()

could use the qualified names of Point components. This is how class Rectangle could decide

whether the x parameter is between the x coordinates of Rectangle data members pt1 and pt2.

bool xIsBetweenBorders = (pt1.x
|| (pt2.x


However, Point data members are private. And the client class (in this example, Rectangle) has

no special privileges accessing the server (Point) components. Class Rectangle has Point objects

as its own components. This is why its methods can access its Point data members (pt1 and pt2).

But methods of class Rectangle cannot access the Point components data members x and y. The

line above is illegal. Rectangle methods should use Point public member functions, for example,

Point::get(), to access Point components.

It is important not to confuse two different contexts. (How many times have I emphasized this?).

The Rectangle class can access its own private Point members pt1 and pt2 without limitations;

the Rectangle class cannot access private components pt1.x, pt1.y, pt2.x, pt2.y of its data

members. This is why Rectangle::pointIn() has to use this code to retrieve data members of

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



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



Rectangle



data members pt1 and pt2.



bool Rectangle::pointIn(int x, int y) const

{ int x1,y1,x2,y2;

// coordinates of corners

pt1.get(x1,y1); pt2.get(x2,y2);

// get point data

bool xIsBetweenBorders = (x1
¡K }

// and so on



The need to use server access functions to access components of class data members is often

annoying. Because of this need, the design of the composite class methods might become quite

cumbersome.

ALERT

If a class has data members that belong to other classes, then the class member functions cannot

access private components of these data members. This is often frustrating, but the composite class

must use data member's methods to get access to components of its own components.



If you look at the design of the client code of class Rectangle, you will see that it does not require

any changes. The client code has to supply five arguments to the Rectangle constructor and two

arguments to the method Rectangle::pointIn(). This means that introduction of the component

class Point benefited the design of composite class Rectangle but did not benefit the design of

client code.

int x1=20, y1=40; int x2=70, y2=90;

//

int x=100, y=120;

//

Rectangle rec(x1,y1,x2,y2,4);

//

rec.setThickness();

//

x -= 25; y -= 15;

//

rec.move(10,20);

//

down

if (rec.pointIn(x,y)) cout << "Point is in\n";



rectangle corners

point to catch with the rectangle

create a Rectangle object

line width is 1 pixel (default)

move the point around the screen

10 pixels to right, 20 pixels

// in point in rectangle?



Again, for this tiny example, the difference is not tremendous, but it is clearly here. Similar to the

first version of class Rectangle, this client code does its processing in terms of separate entities x

and y. The code does not aggregate these separate entities into a class and thus does not pass to the

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



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



maintainer what the code designer knew at the time of design, that these separate entities are related

and represent the coordinates of the same point. Whatever the client code needs to do with points,

like moving, comparing and so on, these actions should be done over individual coordinates in the

client code. The low-level individual actions clutter the client code and make it harder to grasp its

meaning. Consider, for example, the client code statement rec.move(10,20); it says clearly that

the rectangle moves. The fact that the point at coordinates (100,120) is moved has to be deduced

from the series of assignments x -= 25; y -= 15; This responsibility for low-level details is not

pushed to the server code.

Expressing client code in terms of class Point objects and operations over them alleviates these

problems and makes this code more object-oriented.

Point p1(20,40), p2(70,90);

Point point(100,120);

rectangle

Rectangle rec(p1,p2,4);

with this

rec.setThickness();

(default)

point.move(-25,-15);

screen

rec.move(10,20);

pixels down

if (rec.pointIn(point)) cout << "Point is in\n";



// rectangle corners

// point to catch with the

// see below about problems

// line width is 1 pixel

// move the point around the

// 10 pixels to right, 20

// is point in?



This code now has two servers, class Point and class Rectangle. The fact that the Point object

point moves is as clear here as the fact that the Rectangle object rec moves. The responsibility

for low-level details is pushed to servers, and the client code is expressed in terms of selfexplanatory function calls, not in terms of individual computations.

The problem with this code is that it expects the interfaces from the class Rectangle that are not

available. Class Rectangle provides the constructor with five parameters, the client code supplies

only three. Class Rectangle expects two arguments for the function pointIn(), but the client

code supplies only one. This problem could be resolved by changing either the function calls in the

client code or by changing the function interfaces in server Rectangle. The smaller the program,

the less difference it makes what you change.

If class Rectangle were a library class that you cannot change, then there is no choice. It is the

client code that has to work around the limitations of the library. If class Rectangle is one of the

cooperating classes being developed for the application, this class can change, and the decision

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



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



becomes an ideological decision. From the point of view of object-oriented ideology, it is the server

class (in this case, class Rectangle) that has to accommodate the expectations and requirements of

the client code. According to this ideology, class Rectangle should be designed in the following

way.

class Rectangle {

Point pt1, pt2;

// rectangle corners points

int thickness;

// thickness of the rectangle

border

public:

Rectangle (const Point& p1, const Point& p2, int width=1);

void move(int a, int b);

// move both points

void setThickness(int width = 1);

// change thickness

bool pointIn(const Point& pt) const;

// point in rectangle?

. . . . } ;

// the rest of class Rectangle

Rectangle::Rectangle (const Point& p1, const Point& p2, int width)

{ pt1 = p1; pt2 = p2;

thickness = width; }

// set data members

void Rectangle::move(int a, int b)

{ pt1.move(a,b); pt2.move(a,b); }



// pass buck to Point



void Rectangle::setThickness(int width)

{ thickness = width; }



// do the job



bool Rectangle::pointIn(const Point& pt) const //

{ int x,y,x1,y1,x2,y2;

//

pt.get(x,y);

//

pt1.get(x1,y1); pt2.get(x2,y2);

//

bool xIsBetweenBorders = (x1
bool yIsBetweenBorders = (y>y1 && y
return (xIsBetweenBorders && yIsBetweenBorders);



is point in?

coordinates of pt and corners

get parameter's coordinates

get both corners

&& x
&& y>y2);

}



Notice the use of the const keyword (and its absence) in the design of classes Point and

Rectangle. They reflect the changes (and absence of changes) to the target object and to function

call parameters. Since member functions in this design do not return pointers or references or

objects, there is no need to use the const keyword for return values.

The general constructor of class Rectangle can be called with either two or three parameters.

When it is called with two parameters, the thickness data member is set to its default value 1.



Access to Data Members of Method Parameters

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



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



Notice that the method parameters in C++ are treated similarly to data members of the composite

class.

Member function parameters can be of any type, including class objects. There are no limitations

on parameter modes for class objects: They can be passed by value, by pointer, and by reference;

they can even have the const modifier if necessary.

Access to parameter objects in member functions follows the same rules as for any other object: It

is allowed to public parts only. The parameter is available to the method, but its private components

are not. If the client of the parameter (in this case, the member function) needs access to private

parts of the server (its parameter), the server class member functions should be used.

This is why the Rectangle member function pointIn() uses Point access function get() to

access its parameter's components pt.x and pt.y.

An important exception is made in C++ when another object being accessed is the same class type.

This exception applies when an object is passed as a parameter to a member function of the class to

which the object belongs. If the client class and the server class are both of the same type, the client

object has full access rights to the parameter object components. This version of class Point is too

simple to demonstrate this issue. Let us assume that you wish to add a member function

isSamePoint() to class Point. The function should compare the coordinates of its target object

and its parameter object and return true if they have the same values, false otherwise.

bool Point::isSamePoint(const Point& p) const

{ return x==p.x && y==p.y; }



// compare data



In a sense, access to another instance (in this example, p in isSamePoint()) is taking place within

the class scope of the target object (of type Point). This is why it is allowed.

TIP

When a parameter of a class method is of a class component type, the method cannot access the

parameter's private components and should use the parameter's member functions instead. When a

parameter of a class method is of the same type as the class, the method can access the parameter's

private components directly, without access functions. Using access functions would be

syntactically correct but ugly.



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



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

Chapter 12. Composite Classes: Pitfalls and Advantages

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

×