• No results found

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

Good Encapsulation