Item 20: Avoid data members in the public interface.
First, let's look at this issue from the point of view of consistency. If everything in the public interface is a
function, clients of your class won't have to scratch their heads trying to remember whether to use parentheses
when they want to access a member of your class. They'll just do it, because everything is a function. Over the
course of a lifetime, that can save a lot of head scratching.
You don't buy the consistency argument? How about the fact that using functions gives you much more precise
control over the accessibility of data members? If you make a data member public, everybody has read/write
access to it, but if you use functions to get and set its value, you can implement no access, read-only access, and
read-write access. Heck, you can even implement write-only access if you want to:
class AccessLevels {
public:
int getReadOnly() const{ return readOnly; }
void setReadWrite(int value) { readWrite = value; }
int getReadWrite() const { return readWrite; }
void setWriteOnly(int value) { writeOnly = value; }
private:
int noAccess;
// no access to this
int
int readOnly; // read-only access to
// this int
int readWrite; // read-write access to
// this int
int writeOnly; // write-only access to
// this int
};
Still not convinced? Then it's time to bring out the big gun: functional abstraction. If you implement access to a
data member through a function, you can later replace the data member with a computation, and nobody using
your class will be any the wiser.
For example, suppose you are writing an application in which some automated equipment is monitoring the
speed of passing cars. As each car passes, its speed is computed, and the value is added to a collection of all the
speed data collected so far:
class SpeedDataCollection {
public:
void addValue(int speed);
// add a new data value
double averageSoFar() const;
// return average speed
};
Now consider the implementation of the member function averageSoFar (see also Item M18). One way to
implement it is to have a data member in the class that is a running average of all the speed data so far collected.
Whenever averageSoFar is called, it just returns the value of that data member. A different approach is to have
averageSoFar compute its value anew each time it's called, something it could do by examining each data value
in the collection. (For a more general discussion of these two approaches, see Items M17 and M18.)
The first approach ? keeping a running average ? makes each SpeedDataCollection object bigger, because you
have to allocate space for the data member holding the running average. However, averageSoFar can be
implemented very efficiently; it's just an inline function (see Item 33) that returns the value of the data member.
Conversely, computing the average whenever it's requested will make averageSoFar run slower, but each
SpeedDataCollection object will be smaller.
Who's to say which is best? On a machine where memory is tight, and in an application where averages are
needed only infrequently, computing the average each time is a better solution. In an application where averages
are needed frequently, speed is of the essence, and memory is not an issue, keeping a running average is
preferable. The important point is that by accessing the average through a member function, you can use either
implementation, a valuable source of flexibility that you wouldn't have if you made a decision to include the
running average data member in the public interface.
The upshot of all this is that you're just asking for trouble by putting data members in the public interface, so play
it safe by hiding all your data members behind a wall of functional abstraction. If you do it now, we'll throw in
consistency and fine-grained access control at no extra cost!
First, let's look at this issue from the point of view of consistency. If everything in the public interface is a
function, clients of your class won't have to scratch their heads trying to remember whether to use parentheses
when they want to access a member of your class. They'll just do it, because everything is a function. Over the
course of a lifetime, that can save a lot of head scratching.
You don't buy the consistency argument? How about the fact that using functions gives you much more precise
control over the accessibility of data members? If you make a data member public, everybody has read/write
access to it, but if you use functions to get and set its value, you can implement no access, read-only access, and
read-write access. Heck, you can even implement write-only access if you want to:
class AccessLevels {
public:
int getReadOnly() const{ return readOnly; }
void setReadWrite(int value) { readWrite = value; }
int getReadWrite() const { return readWrite; }
void setWriteOnly(int value) { writeOnly = value; }
private:
int noAccess;
// no access to this
int
int readOnly; // read-only access to
// this int
int readWrite; // read-write access to
// this int
int writeOnly; // write-only access to
// this int
};
Still not convinced? Then it's time to bring out the big gun: functional abstraction. If you implement access to a
data member through a function, you can later replace the data member with a computation, and nobody using
your class will be any the wiser.
For example, suppose you are writing an application in which some automated equipment is monitoring the
speed of passing cars. As each car passes, its speed is computed, and the value is added to a collection of all the
speed data collected so far:
class SpeedDataCollection {
public:
void addValue(int speed);
// add a new data value
double averageSoFar() const;
// return average speed
};
Now consider the implementation of the member function averageSoFar (see also Item M18). One way to
implement it is to have a data member in the class that is a running average of all the speed data so far collected.
Whenever averageSoFar is called, it just returns the value of that data member. A different approach is to have
averageSoFar compute its value anew each time it's called, something it could do by examining each data value
in the collection. (For a more general discussion of these two approaches, see Items M17 and M18.)
The first approach ? keeping a running average ? makes each SpeedDataCollection object bigger, because you
have to allocate space for the data member holding the running average. However, averageSoFar can be
implemented very efficiently; it's just an inline function (see Item 33) that returns the value of the data member.
Conversely, computing the average whenever it's requested will make averageSoFar run slower, but each
SpeedDataCollection object will be smaller.
Who's to say which is best? On a machine where memory is tight, and in an application where averages are
needed only infrequently, computing the average each time is a better solution. In an application where averages
are needed frequently, speed is of the essence, and memory is not an issue, keeping a running average is
preferable. The important point is that by accessing the average through a member function, you can use either
implementation, a valuable source of flexibility that you wouldn't have if you made a decision to include the
running average data member in the public interface.
The upshot of all this is that you're just asking for trouble by putting data members in the public interface, so play
it safe by hiding all your data members behind a wall of functional abstraction. If you do it now, we'll throw in
consistency and fine-grained access control at no extra cost!
Comments
Post a Comment
https://gengwg.blogspot.com/