6.2. Good Class Interfaces

 < Free Open Study > 

The first and probably most important step in creating a high-quality class is creating a good interface. This consists of creating a good abstraction for the interface to represent and ensuring that the details remain hidden behind the abstraction.

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 group of routines that clearly belong together.

You might have a class that implements an employee. It would contain data describing the employee's name, address, phone number, and so on. It would offer services to initialize and use an employee. Here's how that might look.

C++ Example of a Class Interface That Presents a Good Abstraction
class Employee { public:    // public constructors and destructors    Employee();    Employee(       FullName name,       String address,       String workPhone,       String homePhone,       TaxId taxIdNumber,       JobClassification jobClass    );    virtual ~Employee();    // public routines    FullName GetName() const;    String GetAddress() const;    String GetWorkPhone() const;    String GetHomePhone() const;    TaxId GetTaxIdNumber() const;    JobClassification GetJobClassification() const;    ... private:    ... };

Cross-Reference

Code samples in this book are formatted using a coding convention that emphasizes similarity of styles across multiple languages. For details on the convention (and discussions about multiple coding styles), see "Mixed-Language Programming Considerations" in Section 11.4.


Internally, this class might have additional routines and data to support these services, but users of the class don't need to know anything about them. The class interface abstraction is great because every routine in the interface is working toward a consistent end.

A class that presents a poor abstraction would be one that contained a collection of miscellaneous functions. Here's an example:

C++ Example of a Class Interface That Presents a Poor Abstraction

class Program { public:    ...    // public routines    void InitializeCommandStack();    void PushCommand( Command command );    Command PopCommand();    void ShutdownCommandStack();    void InitializeReportFormatting();    void FormatReport( Report report );    void PrintReport( Report report );    void InitializeGlobalData();    void ShutdownGlobalData();    ... private:    ... };


Suppose that a class contains routines to work with a command stack, to format reports, to print reports, and to initialize global data. It's hard to see any connection among the command stack and report routines or the global data. The class interface doesn't present a consistent abstraction, so the class has poor cohesion. The routines should be reorganized into more-focused classes, each of which provides a better abstraction in its interface.

If these routines were part of a Program class, they could be revised to present a consistent abstraction, like so:

C++ Example of a Class Interface That Presents a Better Abstraction
 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 necessarily present good individual abstractions just because the overall class does, but they need to be designed to present good abstractions too. For guidelines on that, see Section 7.2, "Design at the Routine Level."

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 well-defined ADTs.

Here's an example of a class that presents an interface that's inconsistent because its level of abstraction is not uniform:

C++ Example of a Class Interface with Mixed Levels of Abstraction

class EmployeeCenus: public ListContainer { public:    ...    // public routines    void AddEmployee( Employee employee );       <-- 1    void RemoveEmployee( Employee employee );       <-- 1    Employee NextItemInList();       <-- 2    Employee FirstItem();              |    Employee LastItem();       <-- 2    ... private:    ... };

(1)The abstraction of these routines is at the "employee" level.

(2)The abstraction of theseroutines is at the "list" level.


This class is presenting two ADTs: an Employee and a ListContainer. This sort of mixed abstraction commonly arises when a programmer uses a container class or other library classes for implementation and doesn't hide the fact that a library class is used. Ask yourself whether the fact that a container class is used should be part of the abstraction. Usually that's an implementation detail that should be hidden from the rest of the program, like this:

C++ Example of a Class Interface with Consistent Levels of Abstraction
 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 convenient because it supports polymorphism, allowing an external search or sort function that takes a ListContainer object. That argument fails the main test for inheritance, which is, "Is inheritance used only for "is a" relationships?" To inherit from ListContainer would mean that EmployeeCensus "is a" ListContainer, which obviously isn't true. If the abstraction of the EmployeeCensus object is that it can be searched or sorted, that should be incorporated as an explicit, consistent part of the class interface.

If you think of the class's public routines as an air lock that keeps water from getting into a submarine, inconsistent public routines are leaky panels in the class. The leaky panels might not let water in as quickly as an open air lock, but if you give them enough time, they'll still sink the boat. In practice, this is what happens when you mix levels of abstraction. As the program is modified, the mixed levels of abstraction make the program harder and harder to understand, and it gradually degrades until it becomes unmaintainable.

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 color the data-entry cells, so we decided to use a spreadsheet control that did provide that capability.

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 cell coloring. By exposing all 150 routines, the programmer created the possibility that, if we ever wanted to change the underlying implementation, we could find ourselves supporting 150 public routines. The programmer failed to achieve the encapsulation we were looking for, as well as creating a lot more work for himself than necessary.

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 opposite operations. If you have an operation that turns a light on, you'll probably need one to turn it off. If you have an operation to add an item to a list, you'll probably need one to delete an item from the list. If you have an operation to activate a menu item, you'll probably need one to deactivate an item. When you design a class, check each public routine to determine whether you need its complement. Don't create an opposite gratuitously, but do check to see whether you need one.

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 initialized before it's passed to RoutineA." The semantic interface should be documented in comments, but try to keep interfaces minimally dependent on documentation. Any aspect of an interface that can't be enforced by the compiler is an aspect that's likely to be misused. Look for ways to convert semantic interface elements to programmatic interface elements by using Asserts or other techniques.

Beware of erosion of the interface's abstraction under modification As a class is modified and extended, you often discover additional functionality that's needed, that doesn't quite fit with the original class interface, but that seems too hard to implement any other way. For example, in the Employee class, you might find that the class evolves to look like this:

Cross-Reference

For more suggestions about how to preserve code quality as code is modified, see Chapter 24, "Refactoring."


C++ Example of a Class Interface That's Eroding Under Maintenance

class Employee { public:    ...    // public routines    FullName GetName() const;    Address GetAddress() const;    PhoneNumber GetWorkPhone() const;    ...    bool IsJobClassificationValid( JobClassification jobClass );    bool IsZipCodeValid( Address address );    bool IsPhoneNumberValid( PhoneNumber phoneNumber );    SqlQuery GetQueryToCreateNewEmployee() const;    SqlQuery GetQueryToModifyEmployee() const;    SqlQuery GetQueryToRetrieveEmployee() const;    ... private:    ... };


What started out as a clean abstraction in an earlier code sample has evolved into a hodgepodge of functions that are only loosely related. There's no logical connection between employees and routines that check ZIP Codes, phone numbers, or job classifications. The routines that expose SQL query details are at a much lower level of abstraction than the Employee class, and they break the Employee abstraction.

Don't add public members that are inconsistent with the interface abstraction Each time you add a routine to a class interface, ask "Is this routine consistent with the abstraction provided by the existing interface?" If not, find a different way to make the modification and preserve the integrity of the abstraction.

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.

Good Encapsulation

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 prevents you from looking at the details even if you want to.

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 neither. There is no middle ground.

Minimize accessibility of classes and members Minimizing accessibility is one of several rules that are designed to encourage encapsulation. If you're wondering whether a specific routine should be public, private, or protected, one school of thought is that you should favor the strictest level of privacy that's workable (Meyers 1998, Bloch 2001). I think that's a fine guideline, but I think the more important guideline is, "What best preserves the integrity of the interface abstraction?" If exposing the routine is consistent with the abstraction, it's probably fine to expose it. If you're not sure, hiding more is generally better than hiding less.

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 monkey around with Point's data and Point won't necessarily even know when its values have been changed (Riel 1996). However, a Point class that exposes

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 floats x, y, and z, whether Point is storing those items as doubles and converting them to floats, or whether Point is storing them on the moon and retrieving them from a satellite in outer space.

Avoid putting private implementation details into a class's interface With true encapsulation, programmers would not be able to see implementation details at all. They would be hidden both figuratively and literally. In popular languages, including C++, however, the structure of the language requires programmers to disclose implementation details in the class interface. Here's an example:

C++ Example of Exposing a Class's Implementation Details
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.

C++ Example of Hiding a Class's 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 adhere to the contract implied by the class interface. It shouldn't make any assumptions about how that interface will or won't be used, other than what's documented in the interface. Comments like the following one are an indication that a class is more aware of its users than it should be:

-- 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 contributes to managing complexity (Gamma et al. 1995). But, in general, friend classes violate encapsulation. They expand the amount of code you have to think about at any one time, thereby increasing complexity.

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 especially applicable to creation of class interfaces. Even if a routine doesn't quite fit the interface's abstraction, sometimes it's tempting to add a routine to an interface that would be convenient for the particular client of a class that you're working on at the time. But adding that routine is the first step down a slippery slope, and it's better not to take even the first step.

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 discovered that learning how to avoid syntax errors had merely bought me a ticket to a whole new theater of coding errors, most of which were more difficult to diagnose and correct than the syntax errors.

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 user of a class can break encapsulation semantically:

  • 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 coupled.

  • 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 > 


Code Complete
Code Complete: A Practical Handbook of Software Construction, Second Edition
ISBN: 0735619670
EAN: 2147483647
Year: 2003
Pages: 334

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net