|
< Free
|
6.2. Good Class Interfaces
The first and probably most important step in creating a
Good Abstraction
As "Form Consistent Abstractions" in Section 5.3 described, abstraction is the ability to view a complex operation in a simplified form. A class interface provides an abstraction of the implementation that's hidden behind the interface. The class's interface should offer a
You might have a class that implements an employee. It would contain data describing the employee's
C++ Example of a Class Interface That
|
|
Suppose that a class contains routines to work with a command stack, to format reports, to print
If these routines were part of a Program class, they could be revised to present a consistent abstraction, like so:
class Program {
public:
...
// public routines
void InitializeUserInterface();
void ShutDownUserInterface();
void InitializeReports();
void ShutDownReports();
...
private:
...
};
The cleanup of this interface assumes that some of the original routines were moved to other, more appropriate classes and some were converted to private routines used by InitializeUserInterface() and the other routines.
This evaluation of class abstraction is based on the class's collection of public routines—that is, on the class's interface. The routines inside the class don't
The pursuit of good, abstract interfaces gives rise to several guidelines for creating class interfaces.
Present a consistent level of abstraction in the class interface
A good way to think about a class is as the mechanism for implementing the abstract data types described in Section 6.1. Each class should implement one and only one ADT. If you find a class implementing more than one ADT, or if you can't determine what ADT the class implements, it's time to reorganize the class into one or more
Here's an example of a class that presents an interface that's inconsistent because its level of abstraction is not uniform:
|
This class is presenting two ADTs: an
Employee
and a
ListContainer
. This
class EmployeeCenus: public ListContainer {
public:
...
// public routines
void AddEmployee( Employee employee );
<-- 1
void RemoveEmployee( Employee employee );
Employee NextEmployee();
Employee FirstEmployee();
Employee LastEmployee();
<-- 1
...
private:
ListContainer m_EmployeeList;
<-- 2
...
};
(1) The abstraction of all these routines is now at the "employee" level.
(2) That the class uses the ListContainer library is now hidden.
Programmers might argue that inheriting from
ListContainer
is
If you think of the class's public routines as an air lock that keeps water from getting into a
Be sure you understand what abstraction the class is implementing
Some classes are similar enough that you must be careful to understand which abstraction the class interface should capture. I once worked on a program that needed to allow information to be edited in a table format. We wanted to use a simple grid control, but the grid controls that were available didn't allow us to
The spreadsheet control was far more complicated than the grid control, providing about 150 routines to the grid control's 15. Since our goal was to use a grid control, not a spreadsheet control, we assigned a programmer to write a wrapper class to hide the fact that we were using a spreadsheet control as a grid control. The programmer grumbled quite a bit about unnecessary overhead and bureaucracy, went away, and came back a couple days later with a wrapper class that faithfully exposed all 150 routines of the spreadsheet control.
This was not what was needed. We wanted a grid-control interface that encapsulated the fact that, behind the scenes, we were using a much more complicated spreadsheet control. The programmer should have exposed just the 15 grid-control routines plus a 16th routine that supported
Depending on specific circumstances, the right abstraction might be either a spreadsheet control or a grid control. When you have to choose between two similar abstractions, make sure you choose the right one.
Provide services in pairs with their opposites
Most operations have corresponding, equal, and
Move unrelated information to another class In some cases, you'll find that half a class's routines work with half the class's data and half the routines work with the other half of the data. In such a case, you really have two classes masquerading as one. Break them up!
Make interfaces programmatic rather than semantic when possible
Each interface consists of a programmatic part and a semantic part. The programmatic part consists of the data types and other attributes of the interface that can be enforced by the compiler. The semantic part of the interface consists of the assumptions about how the interface will be used, which cannot be enforced by the compiler. The semantic interface includes considerations such as "
RoutineA
must be called before
RoutineB
" or "
RoutineA
will crash if
dataMember1
isn't
Beware of
Cross-Reference
For more suggestions about how to preserve code quality as code is modified, see Chapter 24, "Refactoring."
|
What started out as a clean abstraction in an earlier code sample has evolved into a hodgepodge of functions that are only loosely
Don't add public
Consider abstraction and cohesion together The ideas of abstraction and cohesion are closely related—a class interface that presents a good abstraction usually has strong cohesion. Classes with strong cohesion tend to present good abstractions, although that relationship is not as strong.
I have found that focusing on the abstraction presented by the class interface tends to provide more insight into class design than focusing on class cohesion. If you see that a class has weak cohesion and aren't sure how to correct it, ask yourself whether the class presents a consistent abstraction instead.
As Section 5.3 discussed, encapsulation is a stronger concept than abstraction. Abstraction helps to manage complexity by providing models that allow you to ignore implementation details. Encapsulation is the enforcer that
Cross-Reference
For more on encapsulation, see "Encapsulate Implementation Details" in Section 5.3.
The two concepts are related because, without encapsulation, abstraction tends to break down. In my experience, either you have both abstraction and encapsulation or you have
Minimize accessibility of classes and members
Minimizing accessibility is one of several rules that are designed to
The single most important factor that distinguishes a well-designed module from a poorly designed one is the degree to which the module hides its internal data and other implementation details from other modules.
—Joshua Bloch
Don't expose member data in public Exposing member data is a violation of encapsulation and limits your control over the abstraction. As Arthur Riel points out, a Point class that exposes
float x; float y; float z;
is violating encapsulation because client code is free to
float GetX(); float GetY(); float GetZ(); void SetX( float x ); void SetY( float y ); void SetZ( float z );
is maintaining perfect encapsulation. You have no idea whether the underlying implementation is in terms of float s x , y , and z , whether Point is storing those items as double s and converting them to float s, or whether Point is storing them on the moon and retrieving them from a satellite in outer space.
Avoid
Class Employee {
public:
...
Employee(
Fullname name,
String Address,
String workphone,
StringhomePhone
TaxId taxIdNumber
JobClassification jobClass
);
...
Fullname GetName() const;
String GetAddress() const;
...
Private:
String m_name;
<-- 1
String m_Address;
int m_jobClass;
<-- 1
...
};
(1) Here are the exposed implementation details.
Including private declarations in the class header file might seem like a small transgression, but it encourages other programmers to examine the implementation details. In this case, the client code is intended to use the Address type for addresses but the header file exposes the implementation detail that addresses are stored as Strings .
Scott Meyers describes a common way to address this issue in Item 34 of Effective C++ , 2d ed. (Meyers 1998). You separate the class interface from the class implementation. Within the class declaration, include a pointer to the class's implementation but don't include any other implementation details.
class Employee {
public:
...
Employee( ... );
...
FullName GetName() const;
String GetAddress() const;
...
private:
EmployeeImplementation *m_implementation;
<-- 1
};
(1) Here the implementation details are hidden behind the pointer.
Now you can put implementation details inside the EmployeeImplementation class, which should be visible only to the Employee class and not to the code that uses the Employee class.
If you've already written lots of code that doesn't use this approach for your project, you might decide it isn't worth the effort to convert a mountain of existing code to use this approach. But when you read code that exposes its implementation details, you can resist the urge to comb through the private section of the class interface looking for implementation clues.
Don't make assumptions about the class's users
A class should be designed and implemented to
-- initialize x, y, and z to 1.0 because DerivedClass blows -- up if they're initialized to 0.0
Avoid friend classes
In a few circumstances such as the State pattern, friend classes can be used in a disciplined way that
Don't put a routine into the public interface just because it uses only public routines The fact that a routine uses only public routines is not a significant consideration. Instead, ask whether exposing the routine would be consistent with the abstraction presented by the interface.
Favor read-time convenience to write-time convenience
Code is read far more times than it's written, even during initial development. Favoring a technique that speeds write-time convenience at the expense of read-time convenience is a false economy. This is
Be very, very wary of semantic violations of encapsulation
At one time I thought that when I learned how to avoid syntax errors I would be home free. I soon
It ain't abstract if you have to look at the underlying implementation to understand what's going on.
—P. J. Plauger
The difficulty of semantic encapsulation compared to syntactic encapsulation is similar. Syntactically, it's relatively easy to avoid poking your nose into the internal workings of another class just by declaring the class's internal routines and data
private
. Achieving semantic encapsulation is another matter entirely. Here are some examples of the ways that a
Not calling Class A's InitializeOperations() routine because you know that Class A's PerformFirstOperation() routine calls it automatically.
Not calling the database.Connect() routine before you call employee.Retrieve( database ) because you know that the employee.Retrieve() function will connect to the database if there isn't already a connection.
Not calling Class A's Terminate() routine because you know that Class A's PerformFinalOperation() routine has already called it.
Using a pointer or reference to ObjectB created by ObjectA even after ObjectA has gone out of scope, because you know that ObjectA keeps ObjectB in static storage and ObjectB will still be valid.
Using Class B's MAXIMUM_ELEMENTS constant instead of using ClassA.MAXIMUM_ELEMENTS , because you know that they're both equal to the same value.
|
The problem with each of these examples is that they make the client code dependent not on the class's public interface, but on its private implementation. Anytime you find yourself looking at a class's implementation to figure out how to use the class, you're not programming to the interface; you're programming through the interface to the implementation. If you're programming through the interface, encapsulation is broken, and once encapsulation starts to break down, abstraction won't be far behind. |
If you can't figure out how to use a class based solely on its interface documentation, the right response is not to pull up the source code and look at the implementation. That's good initiative but bad judgment. The right response is to contact the author of the class and say "I can't figure out how to use this class." The right response on the class-author's part is not to answer your question face to face. The right response for the class author is to check out the class-interface file, modify the class-interface documentation, check the file back in, and then say "See if you can understand how it works now." You want this dialog to occur in the interface code itself so that it will be preserved for future programmers. You don't want the dialog to occur solely in your own mind, which will bake subtle semantic dependencies into the client code that uses the class. And you don't want the dialog to occur interpersonally so that it benefits only your code but no one else's.
Watch for coupling that's too tight "Coupling" refers to how tight the connection is between two classes. In general, the looser the connection, the better. Several general guidelines flow from this concept:
Minimize accessibility of classes and members.
Avoid
friend
classes, because they're tightly
Make data private rather than protected in a base class to make derived classes less tightly coupled to the base class.
Avoid exposing member data in a class's public interface.
Be wary of semantic violations of encapsulation.
Observe the "Law of Demeter" (discussed in Section 6.3 of this chapter).
Coupling goes hand in glove with abstraction and encapsulation. Tight coupling occurs when an abstraction is leaky, or when encapsulation is broken. If a class offers an incomplete set of services, other routines might find they need to read or write its internal data directly. That opens up the class, making it a glass box instead of a black box, and it virtually eliminates the class's encapsulation.
| < Free Open Study > |