I l@ve RuBoard |
Controlling Access to Class Members
In the previous section, I designed class Cylinder that binds together its data members and member functions in a syntactic unit. This syntax repairs two problems with using global functions for object-oriented programming.
First, using global functions for access to data does not force the indication that data and operations belong together. Hence, it is possible to tear apart the functions that belong together and spread them throughout different places in the source code (making the code more difficult for the maintainer to understand and modify). Second, global function names are global. To avoid potential name conflicts, programmers have to coordinate their activities even when parts of the program they are working on are not immediately related. The class syntax clearly indicates that data and functions belong together. The class scope eliminates the potential for function name conflicts.
At the beginning of this chapter I mentioned two other goals of adding the class facility to C++: pushing responsibility from client code to the server functions and control of access to class members.
Pushing responsibilities down from the client code to the server class is accomplished by the right choice of member functions. (Sometimes, the choice of data members is also important.) In Chapter 8, I discussed the example (see Listing 8.8) where the choice of member functions setRadius(),
getRadius(),
setHeight(), and getHeight(), forced the client code to do the work rather than asking the servers to do the work for it. From that point of view, the choice of member functions in Listing 9.2 is better梚nstead of getting the values of radius and height for scaling, printing, or computing volume, the client code asks the objects of class Cylinder to scale and print themselves or compute their volume.
Pushing responsibility down to the servers is an important concept. What is sufficient and what is not is often subjective. I brushed aside the design in Listing 8.8, but it might be quite useful if the class is used as a library utility that has to serve the largest number of users. For some users, the design in Listing 9.2 might be too restrictive梩hey might want to compute the surface of the cylinder, scale it using different factors in different directions, and so on. Yet, for other users, the design in Listing 9.2 might be too general梩hese users might not need the numerical value of cylinder volume, but they might be interested in finding out whether the first cylinder object is smaller than the second (see Listing 8.9 for comparison).
Pushing responsibilities down to server classes will pop up often during the further discussions of class design. In this section, I will discuss the techniques that allow the class designer to control access to class data members and member functions.
Figure 9-2 describes the class Cylinder and its relationship with its client main(). Here the class has three components: data, functions, and the border that separates everything inside the class from everything outside the class. It shows that data are inside the class. Functions are partially inside the class (their implementation) and outside the class (their interfaces that are known to the client).
Figure 9-2. Class Cylinder and its relationship with its client main().
The picture also shows that when the client code needs the values of cylinder fields (e.g., for computing cylinder volume, scaling, printing, or setting the field values), the client code uses member functions getVolume(),
scaleCylinder(), and so on rather than accessing the values of fields radius and height. This is what the dashed line means. It shows that the direct access to data is ruled out.
There are two motivations for barring access to data members. The first objective is to limit the extent of changes to the program when data design changes. If the interfaces of member functions stay the same (and usually it is not difficult to keep them the same when the data design changes), then it is member function implementations that have to change, not the client code. This is important for maintenance. The set of functions that have to change is well defined梩hey are all listed in the class definition, and there is no need to inspect the rest of the program for possible implications.
The second reason for barring direct client access to data members is that the client code expressed in terms of calls to member functions is easier to understand than is the code expressed in terms of detailed computations over field values (provided that the responsibility is pushed to the member functions and they do the work for the client, not just retrieve and set the values of the fields, as is the case with the getHeight() and setHeight() functions).
To achieve these advantages, everything inside the class should be private to the class, not accessible from outside the class, leaving only function interfaces public, accessible from the outside of the class. This would prevent the client code from creating dependencies on server class data. Remember that the word dependency is the dirtiest word in programming. Dependencies between different parts of program code denote:
the need for cooperation among programmers during program development
the need to study and change more code during program maintenance than is necessary
difficulties in reusing code in the same or similar project
Meanwhile, the class design in Listing 9.2 does not enforce any protection against access to data. The client code can access the fields of the Cylinder object instances, developing dependencies on the Cylinder data design, and foregoing essential advantages of using classes.
Cylinder c1, c2; // define program data
c1.setCylinder(10,30); c2.setCylinder(20,30); // use access function
c1.radius = 10; c1.height = 20; . . . // this is still ok!
C++ allows the class designer to use fine control over access rights to class components. You can indicate access rights to each class component (data or function) by using the keywords public,
private, and protected. Here is another version of class Cylinder.
struct Cylinder { // start of class scope
private:
double radius, height; // data is private
public: // operations are public
void setCylinder(double r, double h);
double getVolume(); // compute volume
void scaleCylinder(double factor);
void printCylinder(); // print object state
} ; // end of class scope
The keywords divide the class scope into segments. All data members or function members following the keyword, for example, private, have the same private access mode. In our example, data members radius and height are private, and all member functions are public.
There might be any number of public,
protected, and private segments in any order you want. In this example, I define the radius data member as private, then two member functions as public, then the height data member as private, then two more member functions as public.
struct Cylinder { // start of class scope
private:
double radius; // data is private
public: // operations are public
void setCylinder(double r, double h);
double getVolume();
private:
double height; // data is private
public: // operations are public
void scaleCylinder(double factor);
void printCylinder(); // print object state
} ; // end of class scope
This is a nice element of flexibility, but usually programmers group all class components with the same access rights in the same segment.
In general, class members (either data members or member functions) in public segments are available to the rest of the program as in the previous examples.
Class members (again, both data and functions) in private segments are available to the class member functions only (and to functions with access rights of a friend; I will discuss friends later, in Chapter 10, "Operator Functions: Another Good Idea."
). Using the name of a private class member outside of the class (or friend) scope is a syntax error.
Notice that these rules do not prevent you from making data private and making functions public. However, in traditional C++ class design, data members are made private, and member functions are made public.
Class members in protected segments are available to the class member functions and to member functions of classes that inherit from this one (directly or indirectly). Discussing inheritance now will take us too far from the topic of class syntax; I will do that later.
Client functions (global functions or member functions of other classes) can access private class members only through the functions (if any) in the public part.
Cylinder c1, c2; // define program data
c1.setCylinder(10,30); c2.setCylinder(20,30); // use access function
// c1.radius = 10; c1.height = 20; // this is now a syntax error
if (c1.getVolume() < c2.getVolume()) // another access function
c1.scaleCylinder(1.2); // scale it up
It is the duty of the class designer to provide necessary access to its data to support class clients and to avoid excessive access. If the client code uses the class feature it does not have to use, it develops extra dependencies. Should this feature change, the client code is affected as well. Also, the more features of the class that are made public, the more knowledge the client programmer and maintainer have to acquire to use the class instances productively.
With the use of private access to class data members, the implementation details of the class Cylinder are now hidden; if the names or types of Cylinder fields change, the client code is not affected as long as the Cylinder class interface remains the same. The client code is prevented from developing dependencies on class Cylinder data design. The client programmer (and maintainer) is excused from the need to learn class Cylinder data design.
Usually, it is the data part that is likely to evolve. This is why, in a typical class, data members are private and member functions are public. This enhances modifiability of the program and reusability of class design. Notice that class member functions (whether public or private) can access any data member of the same class, whether public or private.
This is why any group of functions that accesses the same set of data should be bound together as class member functions, and calls to these functions should be used as messages to class instances in the client code. This enhances reusability.
The class is isolated from other parts of the program. Its private parts are outside the reach of other code (similar to local variables in a function or a block).
This property decreases the amount of coordination among design team members and reduces the likelihood of human miscommunication. This enhances program quality.
In all previous examples, I used the keyword struct to define a C++ class. C++ also allows you to use the keyword class for that purpose. Here is an example of class Cylinder that uses the keyword class rather than struct.
class Cylinder { // change from 'struct' to 'class' keyword
private:
double radius, height; // data is still private
public: // operations are public
void setCylinder(double r, double h);
double getVolume();
void scaleCylinder(double factor);
void printCylinder();
} ; // end of class scope
What is the difference between this class definition and the previous class definition? There is none. This class specification defines exactly the same class. The objects of these classes are exactly the same梩here is no difference at all. There are only two differences between the keywords struct and class in C++. One difference is that the keyword struct has only one meaning in C++: It is used for one purpose only (to introduce a programmer-defined type into the program the way I did in the previous examples). Another difference between the keywords struct and class in C++ is in default access rights. In struct (and in union,) default access is public. In class, default access is private. That is all.
Using default access rights allows you to structure the sequence of data fields and member functions differently. In the next version I am responding to the criticism of some programmers who say that class examples that describe data rather than functions first (as I did in previous examples) are hypocritical. The purpose of the class construct is to hide data design from the client code, and it is not a good idea to open the class specification with the description of the so-called "hidden" data. The client code uses public member functions; hence, it is appropriate if they are listed first in the class specification.
struct Cylinder { // some prefer to list public members first
void setCylinder(double r, double h); // operations are public
double getVolume();
void scaleCylinder(double factor);
void printCylinder();
private:
double radius, height; // data is private
} ; // end of class scope
Others feel that understanding data is important for understanding what member functions do. Hence, there is nothing wrong with describing data first. After all, data "hiding" is not about military-type classified information or KGB-like secrecy, where information should be prevented from being known. In programming, information hiding and encapsulation is about preventing the client code from using the information in the client design, not about knowing this information. In this case, if you want to use default access rights, the class keyword is better than struct.
class Cylinder { // some prefer to list data first
double radius, height; // data is still private
public: // operations are public
void setCylinder(double r, double h);
double getVolume();
void scaleCylinder(double factor);
void printCylinder();
} ; // end of class scope
Some programmers say that the keyword struct is inferior to the keyword class, because if you define the class using the default access rights, data will not be protected against use by the client code, and that will defeat encapsulation.
struct Cylinder { // default access rights are used
double radius, height; // data is not protected from client access
void setCylinder(double r, double h); // methods are public
double getVolume();
void scaleCylinder(double factor);
void printCylinder();
} ; // end of class scope
Yes, this class design does defeat encapsulation. But hey, this does not prove that the keyword struct is inferior to the keyword class. If you replace struct with class in this design, the result will be even worse than with the keyword struct. Do you see why?
class Cylinder { // default access rights are used
double radius, height; // data is protected from client access
void setCylinder(double r, double h); // methods are not accessible
double getVolume();
void scaleCylinder(double factor);
void printCylinder();
} ; // end of class scope
This class is not usable at all. Yes, the data fields are now private (and this is fine), but so are member functions, and the client code cannot access them. This is not a very good design.
It is probably better not to rely on defaults and instead specify access rights explicitly. Let us call a spade a spade.
I l@ve RuBoard |
No comments:
Post a Comment