Good Abstraction
CODING HORROR
C++ Example of a Class Interface that Presents a Better Abstraction 370 class Program { 371 public: 372 ... 373 // public routines 374 void InitializeProgram(); 375 void ShutDownProgram(); 376 ... 377 private: 378 ... 379 } 380
The cleanup of this interface assumes that some of these routines were moved to
381
other, more appropriate classes and some were converted to private routines used
382
by InitializeProgram() and ShutDownProgram(). 383
This evaluation of class abstraction is based on the class’s collection of public
384
routines, that is, its class interface. The routines inside the class don’t necessarily
385
present good individual abstractions just because the overall class does, but they
386
need to be designed to present good abstractions, too. For guidelines on that, see
387
Section 7.2, “Design at the Routine Level.”
388
The pursuit of good, abstract interfaces gives rise to several guidelines for
389
creating class interfaces.
390
Present a consistent level of abstraction in the class interface
391
A good way to think about a class is as the mechanism for implementing the
392
abstract data types (ADTs) described in Section 6.1. Each class should
393
implement one and only one ADT. If you find a class implementing more than
394
one ADT, or if you can’t determine what ADT the class implements, it’s time to
395
reorganize the class into one or more well-defined ADTs.
396
Here’s an example of a class the presents an interface that’s inconsistent because
397
its level of abstraction is not uniform:
398
C++ Example of a Class Interface with Mixed Levels of Abstraction
399
class EmployeeList: public ListContainer {
400 public: 401 ... 402 // public routines 403
void AddEmployee( Employee &employee );
404
void RemoveEmployee( Employee &employee );
405 406
Employee NextItemInList( Employee &employee );
407
Employee FirstItem( Employee &employee );
408
Employee LastItem( Employee &employee );
409
CODING HORROR
The abstraction of these routines is at the “employee” level. The abstraction of these routines is at the “list” level.
... 410 private: 411 ... 412 } 413
This class is presenting two ADTs: an Employee and a ListContainer. This sort 414
of mixed abstraction commonly arises when a programmer uses a container class
415
or other library classes for implementation and doesn’t hide the fact that a library
416
class is used. Ask yourself whether the fact that a container class is used should
417
be part of the abstraction. Usually that’s an implementation detail that should be
418
hidden from the rest of the program, like this:
419
C++ Example of a Class Interface with Consistent Levels of Abstraction
420 class EmployeeList { 421 public: 422 ... 423 // public routines 424
void AddEmployee( Employee &employee );
425
void RemoveEmployee( Employee &employee );
426
Employee NextEmployee( Employee &employee );
427
Employee FirstEmployee( Employee &employee );
428
Employee LastEmployee( Employee &employee );
429 ... 430 private: 431 ListContainer m_EmployeeList; 432 ... 433 } 434
Programmers might argue that inheriting from ListContainer is convenient 435
because it supports polymorphism, allowing an external search or sort function
436
that takes a ListContainer object. 437
That argument fails the main test for inheritance, which is, Is inheritance used
438
only for “is a” relationships? To inherit from ListContainer would mean that 439
EmployeeList “is a” ListContainer, which obviously isn’t true. If the abstraction 440
of the EmployeeList object is that it can be searched or sorted, that should be 441
incorporated as an explicit, consistent part of the class interface.
442
If you think of the class’s public routines as an air lock that keeps water from
443
getting into a submarine, inconsistent public routines are leaky panels in the
444
class. The leaky panels might not let water in as quickly as an open air lock, but
445
if you give them enough time, they’ll still sink the boat. In practice, this is what
446
happens when you mix levels of abstraction. As the program is modified, the
447
mixed levels of abstraction make the program harder and harder to understand,
448
and it gradually degrades until it becomes unmaintainable.
449
The abstraction of all these routines is now at the “employee” level.
That the class uses the
ListContainer library is now hidden
Be sure you understand what abstraction the class is implementing
450
Some classes are similar enough that you must be careful to understand which
451
abstraction the class interface should capture. I once worked on a program that
452
needed to allow information to be edited in a table format. We wanted to use a
453
simple grid control, but the grid controls that were available didn’t allow us to
454
color the data-entry cells, so we decided to use a spreadsheet control that did
455
provide that capability.
456
The spreadsheet control was far more complicated than the grid control,
457
providing about 150 routines to the grid control’s 15. Since our goal was to use a
458
grid control, not a spreadsheet control, we assigned a programmer to write a
459
wrapper class to hide the fact that we were using a spreadsheet control as a grid
460
control. The programmer grumbled quite a bit about unnecessary overhead and
461
bureaucracy, went away, and came back a couple days later with a wrapper class
462
that faithfully exposed all 150 routines of the spreadsheet control.
463
This was not what was needed. We wanted a grid-control interface that
464
encapsulate the fact that, behind the scenes, we were using a much more
465
complicated spreadsheet control. The programmer should have exposed just the
466
15 grid control routines plus a 16th routine that supported cell coloring. By
467
exposing all 150 routines, the programmer created the possibility that, if we ever
468
wanted to change the underlying implementation, we could find ourselves
469
supporting 150 public routines. The programmer failed to achieve the
470
encapsulation we were looking for, as well as creating a lot more work for
471
himself than necessary.
472
Depending on specific circumstances, the right abstraction might be either a
473
spreadsheet control or a grid control. When you have to choose between two
474
similar abstractions, make sure you choose the right one.
475
Provide services in pairs with their opposites
476
Most operations have corresponding, equal, and opposite operations. If you have
477
an operation that turns a light on, you’ll probably need one to turn it off. If you
478
have an operation to add an item to a list, you’ll probably need one to delete an
479
item from the list. If you have an operation to activate a menu item, you’ll
480
probably need one todeactivate an item. When you design a class, check each
481
public routine to determine whether you need its complement. Don’t create an
482
opposite gratuitously, but do check to see whether you need one.
483
Move unrelated information to another class
484
In some cases, you’ll find that half a class’s routines work with half the class’s
485
data, and half the routines work with the other half of the data. In such a case,
486
you really have two classes masquerading as one. Break them up!
Beware of erosion of the interface’s abstraction under modification
488
As a class is modified and extended, you often discover additional functionality
489
that’s needed, that doesn’t quite fit with the original class interface, but that
490
seems too hard to implement any other way. For example, in the Employee class, 491
you might find that the class evolves to look like this:
492
C++ Example of a Class Interface that’s Eroding Under Maintenance
493 class Employee { 494 public: 495 ... 496 // public routines 497 FullName GetName(); 498 Address GetAddress(); 499 PhoneNumber GetWorkPhone(); 500 ... 501
Boolean IsJobClassificationValid( JobClassification jobClass );
502
Boolean IsZipCodeValid( Address address );
503
Boolean IsPhoneNumberValid( PhoneNumber phoneNumber );
504 505 SqlQuery GetQueryToCreateNewEmployee(); 506 SqlQuery GetQueryToModifyEmployee(); 507 SqlQuery GetQueryToRetrieveEmployee(); 508 ... 509 private: 510 ... 511 } 512
What started out as a clean abstraction in an earlier code sample has evolved into
513
a hodgepodge of functions that are only loosely related. There’s no logical
514
connection between employees and routines that check zip codes, phone
515
numbers, or job classifications. The routines that expose SQL query details are at
516
a much lower level of abstraction than the Employee class, and they break the 517
Employee abstraction. 518
Don’t add public members that are inconsistent with the interface
519
abstraction
520
Each time you add a routine to a class interface, ask, “Is this routine consistent
521
with the abstraction provided by the existing interface?” If not, find a different
522
way to make the modification, and preserve the integrity of the abstraction.
523
Consider abstraction and cohesion together
524
The ideas of abstraction and cohesion are closely related—a class interface that
525
presents a good abstraction usually has strong cohesion. Classes with strong
526
cohesion tend to present good abstractions, although that relationship is not as
527
strong.
528
CROSS-REFERENCE For
more suggestions about how to preserve code quality as code is modified, See Chapter 24, “Refactoring.”
I have found that focusing on the abstraction presented by the class interface
529
tends to provide more insight into class design than focusing on class cohesion.
530
If you see that a class has weak cohesion and aren’t sure how to correct it, ask
531
yourself whether the class presents a consistent abstraction instead.
532