Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Professional C++ [eng].pdf
Скачиваний:
284
Добавлен:
16.08.2013
Размер:
11.09 Mб
Скачать

Chapter 2

One language technique for writing general-purpose code in C++ is the template. The following example shows a templatized data structure. If you’ve never seen this syntax before, don’t worry! Chapter 11 explains the syntax in depth.

Instead of writing a specific ChessBoard class that stores ChessPieces, as shown earlier, consider writing a generic GameBoard template that can be used for any type of two-dimensional board game such as chess or checkers. You would need only to change the class declaration so that it takes the piece to store as a template parameter instead of hard-coding it in the interface. The template could look something like this:

template <typename PieceType> Class GameBoard {

public:

// This example omits constructors, destructors, and the assignment operator. void setPieceAt(PieceType* piece, int x, int y);

PieceType& getPieceAt(int x, int y); bool isEmpty(int x, int y);

protected:

// This example omits data members.

};

With this simple change in the interface, you now have a generic game board class that you can use for any two-dimensional board game. Although the code change is simple, it is important to make these decisions in the design phase, so that you are able to implement the code effectively and efficiently.

Reusing Ideas

As the baker example illustrates, it would be ludicrous to reinvent recipes for every dish that you make. However, programmers often make an equivalent mistake in their designs. Instead of utilizing existing “recipes,” or patterns, for designing programs, they reinvent these techniques every time they design a program. However, many design patterns appear in myriad different C++ applications. As a C++ programmer, you should familiarize yourself with these patterns so that you can incorporate them effectively into your program designs.

For example, you might want to design your chess program so that you have a single ErrorLogger object that serializes all errors from different components to a log file. When you try to design your ErrorLogger class, you realize that it would be disastrous to have more than one object instantiated from the ErrorLogger class in a single program. You also want to be able to access this ErrorLogger object from anywhere in your program. These requirements of a single, globally accessible, instance of a class arise frequently in C++ programs, and there is a standard strategy to implement them, called the singleton. Thus, a good design at this point would specify that you want to use the singleton pattern. Chapters 5, 25, and 26 cover design patterns and techniques in much more detail.

Designing a Chess Program

This section introduces a systematic approach to designing a C++ program in the context of a simple chess game application. In order to provide a complete example, some of the steps refer to concepts covered in later chapters. You should read this example now, in order to obtain an overview of the design process, but you might also consider rereading it after you have finished reading Part I.

50

Designing Professional C++ Programs

Requirements

Before embarking on the design, it is important to possess clear requirements for the program’s functionality and efficiency. Ideally, these requirements would be documented in the form of a requirements specification. The requirements for the chess program would contain the following types of specifications, although in more detail and number:

The program will support the standard rules of chess.

The program will support two human players. The program will not provide an artificially intelligent computer player.

The program will provide a text-based interface:

The program will render the game board and pieces in ASCII text.

Players will express their moves by entering numbers representing locations on the chessboard.

The requirements ensure that you design your program so that it performs as its users expect. You would not want to waste time designing and coding a graphical user interface for the chess game if the users desire only a text-based interface. Conversely, it would be important to be aware of a user preference for a graphical user interface, so that you don’t design the program in a way that precludes that possibility.

Design Steps

You should take a systematic approach to designing your program, working from the general to the specific. The following steps do not always apply to all programs, but they provide a general guideline. Your design should include diagrams and tables as appropriate. This example includes sample diagrams and tables. Feel free to follow the format used here or to invent your own.

There is no “right” way to draw software design diagrams as long as they are clear and meaningful to yourself and your colleagues.

Divide the Program into Subsystems

Your first step is to divide your program into its general functional subsystems and to specify the interfaces and interactions between the subsystems. At this point, you should not worry about specifics of data structures and algorithms, or even classes. You are trying only to obtain a general feel for the various parts of the program and their interactions. You can list the subsystems in a table that expresses the high-level behaviors or functionality of the subsystem, the interfaces exported from the subsystem to other subsystems, and the interfaces consumed, or used, by this subsystem on other subsystems. A table for the chess game subsystems could look like this:

51

Chapter 2

Subsystem Name

Number

Functionality

Interfaces Exported

Interfaces Consumed

 

 

 

 

 

GamePlay

1

Starts game

Game Over

Take Turn (on Player)

 

 

Controls game

 

Draw (on Chess

 

 

flow

 

Board)

 

 

Controls drawing

 

 

 

 

Declares winner

 

 

 

 

Ends game

 

 

Chess Board

1

Stores chess pieces

Get Piece At

Game Over (on

 

 

Checks for draws

Set Piece At

Game-Play)

 

 

and checkmates

Draw

Draw (on Chess

 

 

Draws itself

 

Piece)

Chess Piece

32

Draws itself

Draw

Get Piece At (on

 

 

Moves itself

Move

Game Board)

 

 

Checks for legal

Check Move

Set Piece At (on

 

 

moves

 

Game Board)

Player

2

Interacts with

Take Turn

Get Piece At (on

 

 

user: prompts

 

Game Board)

 

 

user for move,

 

Move (on Chess

 

 

obtains user’s

 

Piece)

 

 

move

 

Check Move (on

 

 

Moves pieces

 

Chess Piece)

ErrorLogger

1

Writes error mes-

Log Error

None

 

 

sages to log file

 

 

 

 

 

 

 

As this table shows, the functional subsystems of this chess game include a GamePlay subsystem, a ChessBoard, 32 ChessPieces, two Players, and one ErrorLogger. However, that is not the only reasonable approach to a Chess Game. In software design, as in programming itself, there are often many different ways to accomplish the same goal. Not all ways are equal: some are certainly better than others. However, there are often several equally valid methods.

A good division into subsystems separates the program into its basic functional parts. For example, in the Chess Game, a Player is a subsystem distinct from the Board, Chess Pieces, or GamePlay. It wouldn’t make sense to lump the players into the GamePlay object because they are logically separate subsystems. Other choices are not as obvious. Would it be reasonable to add a separate User Interface subsystem?

If you intend to provide different kinds of user interfaces, or want to easily modify your interfaces later, you might want to separate out this aspect into a separate subsystem. To make these kinds of choices, you need to consider not only the current goals of the program, but future goals as well.

Because it is often difficult to visualize subsystem relationships from tables, it is usually helpful to show the subsystems of a program in a diagram, such as Figure 2-1. In this figure, arrows represent calls from one subsystem to another (for simplicity, the Error Logger subsystem is omitted).

52

Designing Professional C++ Programs

GamePlay

Game Draw

Over

ChessBoard

Figure 2-1

TakeTurn

GetPieceAt

Draw

GetPieceAt

SetPieceAt

Player

Move

CheckMove

Chess

Piece

Choose Threading Models

In this step, you choose the number of threads in your program and specify their interactions. You should also specify any locking for shared data. If you are unfamiliar with multithreaded programs, or your platform does not support multithreading, then you should make your programs single-threaded. However, if your program has several distinct tasks, each of which should work in parallel, it might be a good candidate for multiple threads. For example, graphical user interface applications often have one thread performing the main application work and another thread waiting for the user to press buttons or select menu items.

Because threading is platform specific, this book does not cover multithreaded programming. See Chapter 18 for a discussion of platform considerations with C++.

The chess program needs only one thread to control the game flow.

Specify Class Hierarchies for Each Subsystem

In this step, you determine the class hierarchies that you intend to write in your program. The chess program needs only one class hierarchy, to represent the chess pieces. The hierarchy could work as shown in Figure 2-2.

ChessPiece

Rook

Bishop

Knight

King

Pawn

Queen

Figure 2-2

53

Chapter 2

In this hierarchy, a generic ChessPiece class serves as the superclass. The hierarchy uses multiple inheritance to show that the queen piece is a combination of a rook and a bishop.

Chapter 3 explains the details of designing classes and class hierarchies.

Specify Classes, Data Structures, Algorithms, and Patterns for Each Subsystem

In this step, you consider a greater level of detail, and specify the particulars of each subsystem, including the specific classes that you write for each subsystem. It may well turn out that you model each subsystem itself as a class. This information can again be summarized in a table:

Subsystem

Classes

Data Structures

Algorithms

Patterns

 

 

 

 

 

GamePlay

GamePlay class

GamePlay object

Simple loop giving

None

 

 

includes one

each player a turn

 

 

 

ChessBoard

object to play

 

 

 

and two Player

 

 

 

 

objects

 

 

ChessBoard

ChessBoard class

ChessBoard object

Checks for win or

None

 

 

is a two-dimensional

draw after each

 

 

 

array of ChessPieces

move

 

 

 

ChessBoard stores

 

 

 

 

32 ChessPieces

 

 

ChessPiece

ChessPiece

Each piece stores its

Piece checks for

None

 

abstract superclass

location in the chess

legal move by

 

 

Rook, Bishop,

board

querying chess

 

 

Knight, King,

 

board for pieces

 

 

Pawn, and Queen

 

at various

 

 

classes

 

locations

 

Player

One Player class

Two player objects

Take Turn algorithm:

None

 

 

(black and white)

loop to prompt user

 

 

 

 

for move, check if

 

 

 

 

move is legal, and

 

 

 

 

move piece

 

ErrorLogger

One ErrorLogger

A queue of messages

Buffers messages and

Singleton

 

class

to log

writes them to a log

pattern

 

 

 

file periodically

to ensure

 

 

 

 

only one

 

 

 

 

ErrorLogger

 

 

 

 

object

 

 

 

 

 

This section of the design document would normally present the actual interfaces for each class, but this example will forgo that level of detail.

54

Designing Professional C++ Programs

Designing classes and choosing data structures, algorithms, and patterns can be tricky. You should always keep in mind the rules of abstraction and reuse discussed earlier in this chapter. For abstraction, the key is to consider the interface and the implementation separately. First, specify the interface from the perspective of the user. Decide what you want the component to do. Then decide how the component will do it by choosing data structures and algorithms. For reuse, familiarize yourself with standard data structures, algorithms, and patterns. Also, make sure you are aware of the standard library code in C++, as well as any proprietary code available in your workplace.

Chapters 3, 4, and 5 discuss these issues in more detail.

Specify Error Handling for Each Subsystem

In this design step, you delineate the error handling in each subsystem. The error handling should include both system errors, such as memory allocation failures, and user errors, such as invalid entries. You should specify whether each subsystem uses exceptions. You can again summarize this information in a table:

Subsystem

Handling System Errors

Handling User Errors

GamePlay

Logs an error with the ErrorLogger and

 

terminates program if unable to allocate

 

memory for ChessBoard or Players

Not applicable (no direct user interface)

ChessBoard

Logs an error with the ErrorLogger and

 

throws an exception if unable to allocate

 

memory for itself or for its ChessPieces

Not applicable (no direct user interface)

ChessPiece

Logs an error with the ErrorLogger and

 

throws an exception if unable to allocate

 

memory

Not applicable (no direct user interface)

Player

Logs an error with the ErrorLogger and

 

throws an exception if unable to allocate

 

memory.

ErrorLogger

Attempts to log an error and terminates

 

the program if unable to allocate memory

Sanity-checks user move entry to ensure that it is not off the board; prompts user for another entry Checks each move legality before moving the piece; if illegal, prompts user for another move

Not applicable (no direct user interface)

The general rule for error handling is to handle everything. Think hard about all possible error conditions. If you forget one possibility, it will show up as a bug in your program! Don’t treat anything as an “unexpected” error. Expect all possibilities: memory allocation failures, invalid user entries, disk failures, and network failures, to name a few. However, as the table for the chess game shows, you should handle user errors differently from internal errors. For example, a user entering an invalid move should not cause your chess program to terminate.

Chapter 15 discusses error handling in more depth.

55

Chapter 2

Summar y

In this chapter, you learned about the professional C++ approach to design. Hopefully, it convinced you that software design is an important first step in any programming project. You also learned about some of the aspects of C++ that make design difficult, including its object-oriented focus, its large feature set and standard library, and its facilities for writing generic code. With this information, you are better prepared to tackle C++ design.

This chapter introduced two design themes. The concept of abstraction, or separating interface from implementation, permeates this book and should be a guideline for all your design work. The notion of reuse, both of code and ideas, also arises frequently in real-world projects, and in this text. You should reuse existing code and ideas, and write your code to be as reusable as possible.

Now that you understand the importance of design and the basic design themes, you are ready for the rest of Part I. Chapter 3 describes strategies for utilizing the object-oriented aspects of C++ in your design. Chapters 4 and 5 present guidelines both for reusing preexisting code and ideas and for writing reusable code. Chapter 6 closes Part I with a discussion of software-engineering models and processes.

56