Day 18
Object-Oriented Analysis and Design
It is easy to become focused on the syntax of C++ and to lose sight of
how and why you use these techniques to build programs. Today you will learn
How to
analyze problems from an object-oriented perspective.
How to
design your program from an object-oriented perspective.
How to
design for reusability and extensibility.
The Development Cycle
Many volumes have been written about the development cycle. Some propose
a "waterfall" method, in which designers determine what the program
should do; architects determine how the program will be built, what classes
will be used, and so forth; and then programmers implement the design and
architecture. By the time the design and architecture is given to the
programmer, it is complete; all the programmer needs to do is implement the
required functionality.
Even if
the waterfall method worked, it would probably be a poor method for writing
good programs. As the programmer proceeds, there is a necessary and natural
feedback between what has been written so far and what remains to be done.
While it is true that good C++ programs are designed in great detail before a
line of code is written, it is not true that that design remains unchanged
throughout the cycle.
The amount of design that must be finished "up front," before
programming begins, is a function of the size of the program. A highly complex
effort, involving dozens of programmers working for many months, will require a
more fully articulated architecture than a quick-and-dirty utility written in
one day by a single programmer.
This chapter will focus on the design of large, complex programs which
will be expanded and enhanced over many years. Many programmers enjoy working
at the bleeding edge of technology; they tend to write programs whose
complexity pushes at the limits of their tools and understanding. In many ways,
C++ was designed to extend the complexity that a programmer or team of
programmers could manage.
This chapter will examine a number of design problems from an
object-oriented perspective. The goal will be to review the analysis process,
and then to understand how you apply the syntax of C++ to implement these
design objectives.
Simulating an Alarm System
A simulation is a computer model of a part of a real-world system. There
are many reasons to build a simulation, but a good design must start with an
understanding of what questions you hope the simulation will answer.
As a starting point, examine this problem: You have been asked to
simulate the alarm system for a house. The house is a center hall colonial with
four bedrooms, a finished basement, and an under-the-house garage.
The downstairs has the following windows: three in the kitchen, four in
the dining room, one in the half-bathroom, two each in the living room and the
family room, and two small windows next to the door. All four bedrooms are
upstairs, each of which has two windows except for the master bedroom, which
has four. There are two baths, each with one window. Finally, there are four
half-windows in the basement, and one window in the garage.
Normal access to the house is through the front door. Additionally, the
kitchen has a sliding glass door, and the garage has two doors for the cars and
one door for easy access to the basement. There is also a cellar door in the
backyard.
All the windows and doors are alarmed, and there are panic buttons on
each phone and next to the bed. The grounds are alarmed as well, though these
are carefully calibrated so that they are not set off by small animals or
birds.
There is a central alarm system in the basement,
which sounds a warning chirp when the alarm has been tripped. If the alarm is
not disabled within a setable amount of time, the police are called. If a panic
button is pushed, the police are called immediately.
The alarm is also wired into the fire and smoke detectors and the
sprinkler system, and the alarm system itself is fault tolerant, has its own
internal backup power supply, and is encased in a fireproof box.
Preliminary Design
You begin
by asking, "What questions might this simulation answer?" For
example, you might be able to use the simulation to answer the questions,
"How long might a sensor be broken before anyone notices?" or
"Is there a way to defeat the window alarms without the police being notified?"
Once you understand the purpose of the simulation you will know what
parts of the real system the program must model. Once that is well understood,
it becomes much easier to design the program itself.
What Are the Objects?
One way to approach this problem
is to set aside issues relating to the user interface and to focus only
on the components of the
"problem space." A first approximation of an object-oriented design
might be to list the objects that you need to simulate, and then to examine what
these objects "know" and "do."
New Term: The problem space is the set of
problems and issues your program is trying to solve. The solution space is the set of possible solutions to the
problems.
For example, clearly you have sensors of various types,
a central alarm system, buttons, wires, and telephones. Further thought
convinces you that you must also simulate rooms, perhaps floors, and possibly
groups of people such as owners and police.
The sensors can be divided into motion detectors,
trip wires, sound detectors, smoke detectors, and so forth. All of these are
types of sensors, though there is no such thing as a sensor per se. This is a
good indication that sensor is an abstract data type (ADT).
As an ADT, the class sensor would provide the complete interface for all
types of sensors, and each derived type would provide the implementation.
Clients of the various sensors would use them without regard to which type of
sensor they are, and they would each "do the right thing" based on
their real type.
To create a good ADT, you need to have a complete understanding of what
sensors do (rather than how they work). For example, are sensors passive
devices or are they active? Do they wait for some element to heat up, a wire to
break, or a piece of caulk to melt, or do they probe their environment? Perhaps
some sensors have only a binary state (alarm state or okay), but others have a
more analog state (what is the current temperature?). The interface to the
abstract data type should be sufficiently complete to handle all the
anticipated needs of the myriad derived classes.
Other Objects
The
design continues in this way, teasing out the various other classes that will
be required to meet the specification. For example, if a log is to be kept,
probably a timer will be needed; should the timer poll each sensor or should
each sensor file its own report periodically?
The user is going to need to be able to set up, disarm, and program the
system, and so a terminal of some sort will be required. You may want a
separate object in your simulation for the alarm program itself.
What Are the Classes?
As you solve these problems, you will begin to design your classes. For
example, you already have an indication that HeatSensor will derive from Sensor. If the
sensor is to make periodic reports, it may also derive via multiple inheritance
from Timer, or it may have a timer as a
member variable.
The HeatSensor will
probably have member functions such as CurrentTemp() and SetTempLimit() and will
probably inherit functions such as SoundAlarm()
from its base class,
Sensor.
A frequent issue in object-oriented design is that of encapsulation. You
could imagine a design in which the alarm system has a setting for MaxTemp. The alarm system asks the heat sensor what the
current temperature is, compares it to the maximum temperature, and
sounds the alarm if it is too hot. One could argue that this violates the
principle of encapsulation. Perhaps it would be better if the
alarm system didn't know or care what the details are of temperature
analysis; arguably that should be in the HeatSensor.
Whether or not you agree with that argument, it is the kind of decision
you want to focus on during the analysis of the problem. To continue this
analysis, one could argue that only the Sensor and the
Log
object should know any details of how sensor
activity is logged; the Alarm object
shouldn't know or care.
Good encapsulation is marked by each class having a coherent and
complete set of responsibilities, and no other class having the same
responsibilities. If the sensor is responsible for noting the current
temperature, no other class should have that responsibility.
On the other hand, other classes might help deliver the necessary
functionality. For example, while it might be the responsibility of the Sensor class to note and log the current temperature, it might
implement that responsibility by
delegating to a Log object the job of actually recording the data.
Maintaining a firm division of responsibilities makes your program easier
to extend and maintain. When you decide to change the alarm system for an
enhanced module, its interface to the log and to
the sensors will be narrow and well defined. Changes to the alarm system
should not affect the Sensor classes,
and vice versa.
Should the HeatSensor have a ReportAlarm() function? All sensors will need the ability to report an alarm. This is
a good indication that ReportAlarm() should
be a virtual method of Sensor, and
that Sensor may be an
abstract base class. It is possible that HeatSensor will chain up to Sensor's more general ReportAlarm() method;
the overridden function would just fill in
the
details it is uniquely qualified to supply.
How Are Alarms Reported?
When your sensors report an alarm condition, they will want to provide a
lot of information to the object that phones the police and to the log. It may
well be that you'll want to create a Condition
class, whose constructor takes a number of measurements. Depending on
the complexity of the measurements, these too might be objects, or they might
be simple scalar values such as integers.
It is possible that Condition objects
are passed to the central Alarm object,
or that Condition objects
are subclassed into Alarm objects,
which themselves know how to take emergency action. Perhaps there is no central
object; instead there might be sensors, which know how to create
Condition
objects. Some Condition objects would know how to log themselves; others might
know how to contact the police.
A well-designed event-driven system need not have a central coordinator.
One can imagine the sensors all independently receiving and sending message
objects to one another, setting parameters, taking readings, and monitoring the
house. When a fault is detected, an Alarm object is created, which logs the problem (by sending a message to the Log object) and takes the appropriate action.
Event Loops
To
simulate such an event-driven system, your program needs to create an event
loop. An event loop is typically an infinite loop such as while(1), which gets messages from the operating system
(mouse clicks, keyboard presses, and so on) and dispatches them one by
one, returning to the loop until an exit condition is satisfied. Listing 18.1
shows a rudimentary event loop.
Listing 18.1. A simple event loop.
1: //
Listing 18.1
2:
3: #include
<iostream.h>
4:
class
Condition
{
public:
Condition()
{ }
virtual
~Condition() {}
virtual
void Log() = 0;
};
12:
class
Normal : public Condition
{
public:
Normal()
{ Log(); }
virtual
~Normal() {}
virtual void Log() { cout <<
"Logging normal conditions...\n"; }
};
20:
class
Error : public Condition
{
public:
Error()
{Log();}
virtual
~Error() {}
virtual
void Log() { cout << "Logging error!\n"; }
};
class
Alarm : public Condition
{
public:
Alarm
();
virtual ~Alarm() {}
virtual
void Warn() { cout << "Warning!\n"; }
virtual
void Log() { cout << "General Alarm log\n"; }
virtual
void Call() = 0;
37:
38: };
39:
Alarm::Alarm()
{
Log();
Warn();
}
class
FireAlarm : public Alarm
{
public:
FireAlarm(){Log();};
virtual
~FireAlarm() {}
virtual
void Call() { cout<< "Calling Fire Dept.!\n"; }
virtual
void Log() { cout << "Logging fire call.\n"; }
};
53:
int
main()
{
int
input;
int
okay = 1;
Condition
* pCondition;
while
(okay)
{
61:
|
cout << "(0)Quit (1)Normal (2)Fire: ";
|
62:
|
cin
>> input;
|
63:
|
okay
= input;
|
64:
|
switch
(input)
|
65:
|
{
|
66:
|
case
0: break;
|
67:
|
case
1:
|
68:
|
pCondition
= new Normal;
|
69:
|
delete
pCondition;
|
70:
|
break;
|
71:
|
case
2:
|
72:
|
pCondition
= new FireAlarm;
|
73:
|
delete
pCondition;
|
break;
|
|
75:
|
default:
|
76:
|
pCondition = new Error;
|
77:
|
delete
pCondition;
|
78:
|
okay
= 0;
|
79:
|
break;
|
80:
|
}
|
}
return
0;
}
Output: (0)Quit
(1)Normal (2)Fire: 1
Logging normal
conditions...
(0)Quit (1)Normal (2)Fire: 2 General Alarm log
Warning!
Logging fire call.
(0)Quit (1)Normal
(2)Fire: 0
Analysis: The simple loop
created on lines 59-80 allows the user to enter input simulating a normal
report from a sensor and a report of a fire. Note that the effect of
this report is to spawn a Condition object
whose constructor calls various member functions.
Calling
virtual member functions from a constructor can cause confusing results if you
are not mindful of the order of construction of objects. For example, when the FireAlarm object is created on line 72, the order of construction is Condition, Alarm, FireAlarm. The Alarm
constructor calls Log, but it is Alarm's Log(), not FireAlarm's, that is invoked, despite Log() being declared virtual. This is because at the time Alarm's constructor runs, there is no FireAlarm object. Later, when FireAlarm itself
is constructed, its constructor calls Log() again, and this time
FireAlarm::Log() is
called.
PostMaster
Here's another problem on which to practice your object-oriented
analysis: You have been hired by Acme Software, Inc., to start a new software
project and to hire a team of C++ programmers to implement your program. Jim
Grandiose, vice-president of new product development, is your new boss. He
wants you to design and build PostMaster, a utility to read electronic mail
from various unrelated e-mail providers. The potential customer is a
businessperson who uses more than one e-mail product, for example Interchange,
CompuServe, Prodigy, America Online, Delphi, Internet Mail, Lotus Notes,
AppleMail, cc:Mail, and so forth.
The customer will be able to "teach"
PostMaster how to dial up or otherwise connect to each of the e-mail providers,
and PostMaster will get the mail and then present it in a uniform manner, allowing
the customer to organize the mail, reply, forward letters among services, and
so forth.
PostMasterProfessional, to be developed as version 2 of PostMaster, is
already anticipated. It will add an Administrative Assistant mode, which will
allow the user to designate another person to read some or all of the mail, to
handle routine correspondence, and so forth. There is also speculation in the
marketing department that an artificial intelligence component might add the
capability for PostMaster to pre-sort and prioritize the mail based on subject
and content keywords and associations.
Other enhancements have been talked about, including the ability to
handle not only mail but discussion groups such as Interchange discussions,
CompuServe forums, Internet newsgroups, and so forth. It is obvious that Acme
has great hopes for PostMaster, and you are under severe time constraints to
bring it to market, though you seem to have a nearly unlimited budget.
Measure Twice, Cut Once
You set
up your office and order your equipment, and then your first order of business
is to get a good specification for the product. After examining the market, you
decide to recommend that development be focused on a single platform, and you
set out to decide among DOS; UNIX; the Macintosh; and Windows, Windows NT, and
OS/2.
You have many painful meetings with Jim Grandiose, and it becomes clear
that there is no right choice, and so you decide to separate the front end,
that is the user interface or UI, from the back end, the communications and
database part. To get things going quickly, you decide to write for DOS first,
followed by Win32, the Mac, and then UNIX and OS/2.
This simple decision has enormous ramifications for your project. It
quickly becomes obvious that you will need a class library or a series of
libraries to handle memory management, the various user interfaces, and perhaps
also the communications and database components.
Mr. Grandiose believes strongly that projects live
or die by having one person with a clear vision, so he asks that you do the
initial architectural analysis and design before hiring any programmers. You
set out to analyze the problem.
Divide and Conquer
It quickly becomes obvious that you really have more than one problem to
solve. You divide the project into these significant sub-projects:
Communications: the ability for
the software to dial into the e-mail provider via modem, or to connect over a
network.
Database: the ability to store
data and to retrieve it from disk.
E-mail: the ability to read
various e-mail formats and to write new messages to each system.
Editing: providing state-of-the-art editors for the
creation and manipulation of messages.
Extensibility: planning for growth and
enhancements.
Organization and scheduling:
managing the various developers and their code interdependencies. Each group
must devise and publish schedules, and then be able to plan accordingly. Senior
management and marketing need to know when the product will be ready.
You decide to hire a manager to handle item 7, organization and
scheduling. You then hire senior developers to help you analyze and design, and
then to manage the implementation of the remaining areas. These senior
developers will create the following teams:
Communications: responsible for
both dial-up and network communications. They deal with packets, streams, and
bits, rather than with e-mail messages per se.
Message format: responsible for converting
messages from each e-mail provider to a canonical form (PostMaster standard)
and back. It is also their job to write these messages to disk and to get them
back off the disk as needed.
Message editors: This group is
responsible for the entire UI of the product, on each platform. It is their job
to ensure that the interface between the back end and the front end of the
product is sufficiently narrow that extending the product to other platforms
does not require duplication of code.
Message Format
You decide to focus on the message format first, setting aside the
issues relating to communications and user interface. These will follow once
you understand more fully what it is you are dealing with. There is little
sense in worrying about how to present the information to the user until you
understand what information you are dealing with.
An examination of the various e-mail formats reveals that they have many
things in common, despite their various differences. Each e-mail message has a
point of origination, a destination, and a creation date. Nearly all such
messages have a title or subject line and a body which may consist of simple
text, rich text (text with formatting), graphics, and perhaps even sound or
other fancy additions. Most such e-mail services also support attachments, so
that users can send programs and other files.
You confirm your early decision that you will read each mail message out
of its original format and into PostMaster format. This way you will only have
to store one record format, and writing to and reading from the disk will be
simplified. You also decide to separate the "header" information
(sender, recipient, date, title, and so on) from the body of the message. Often
the user will want to scan the headers without necessarily reading the contents
of all the messages. You anticipate that a time may come when users will want
to download only the headers from the message provider, without getting the
text at all, but for now you intend that version 1 of PostMaster will always
get the full message, although it may not display it to the user.
This analysis of the messages leads you to design the Message class. In anticipation of extending the program to non-e-mail messages,
you derive EmailMessage from the
abstract base Message. From EmailMessage you derive PostMasterMessage, InterchangeMessage,
CISMessage,
ProdigyMessage, and so forth.
Messages
are a natural choice for objects in a program handling mail messages, but
finding all the right objects in a complex system is the single greatest
challenge of object-oriented programming. In some cases, such as with messages,
the primary objects seem to "fall out" of your understanding of the
problem. More often, however, you have to think long and hard about what you
are trying to accomplish to find the right objects.
Don't despair. Most designs are not perfect the first time. A good
starting point is to describe the problem out loud. Make a list of all the
nouns and verbs you use when describing the project. The nouns are good
candidates for objects. The verbs might be the methods of those objects (or
they may be objects in their own right). This is not a foolproof method, but it
is a good technique to use when getting started on your design.
That was the easy part. Now the question arises,
"Should the message header be a separate class from the body?" If so,
do you need parallel hierarchies, CompuServeBody and CompuServeHeader, as well
as ProdigyBody and ProdigyHeader?
Parallel hierarchies are often a warning sign of a bad design. It is a
common error in object-oriented design to have a set of objects in one
hierarchy, and a matching set of "managers" of those objects in
another. The burden of keeping these hierarchies up-to-date and in sync with each
other soon becomes overwhelming: a classic maintenance nightmare.
There are
no hard-and-fast rules, of course, and at times such parallel hierarchies are
the most efficient way to solve a particular problem. Nonetheless, if you see
your design moving in this direction, you should rethink the problem; there may
be a more elegant solution available.
When the messages arrive from the e-mail provider, they will not
necessarily be separated into header and body; many will be one large stream of
data, which your program will have to disentangle. Perhaps your hierarchy
should reflect that idea directly.
Further reflection on the tasks at hand leads you to try to list the
properties of these messages, with an eye towards introducing capabilities and
data storage at the right level of abstraction. Listing properties of your
objects is a good way to find the data members, as well as to "shake
out" other objects you might need.
Mail messages will need to be stored, as will the
user's preferences, phone numbers, and so forth. Storage clearly needs to be
high up in the hierarchy. Should the mail messages necessarily share a base
class with the preferences?
There are two overall approaches to inheritance hierarchies: you can have
all, or nearly all, of your classes descend from a common root class, or you
can have more than one inheritance hierarchy. An advantage of a common root
class is that you often can avoid multiple inheritance; a disadvantage is that
many times implementation will percolate up into the base class.
New Term: A set of classes is rooted if all share a common ancestor. Non-rooted hierarchies do not all share a
common base class.
Because
you know that your product will be developed on many platforms, and because
multiple inheritance is complex and not necessarily well supported by all
compilers on all platforms, your first decision is to use a rooted hierarchy
and single inheritance. You decide to identify those places where multiple
inheritance might be used in the future, and to design so that breaking apart
the hierarchy and adding multiple inheritance at a later time need not be
traumatic to your entire design.
You decide to prefix the name of all of your internal classes with the
letter p so that you can easily and
quickly tell which classes are yours and which are from other libraries. On Day
21, "What's Next," you'll learn about name spaces, which can
reinforce this idea, but for now the initial will do nicely.
Your root class will be pObject; virtually
every class you create will descend from this object. pObject
itself will be kept fairly simple; only that data
which absolutely every item shares will
appear in
this class.
If you want a rooted hierarchy, you'll want to give the root class a fairly
generic name (like pObject) and few
capabilities. The point of a root object is to be able to create collections of
all its descendants and refer to them as
instances of pObject. The
trade-off is that rooted hierarchies often
percolate interface up into the root class. You will pay the price; by
percolating these interfaces up into the root object, other descendants will
have interfaces that are inappropriate to their design. The only good solution
to this problem, in single inheritance, is to use templates. Templates are
discussed tomorrow.
The next likely candidates for top of the hierarchy status are pStored and pWired. pStored objects are saved to disk at various times (for example when the
program is not in use), and pWired
objects are sent over the modem or network. Because nearly all of your
objects will need to be stored to disk, it makes sense to push this
functionality up high in the hierarchy. Because all the objects that
are sent over the modem must be stored, but not all stored objects must
be sent over the wire, it makes sense to derive pWired from pStored.
Each derived class acquires all the knowledge (data) and functionality
(methods) of its base class, and each should add one discrete additional
ability. Thus, pWired may add
various methods, but all are
It is possible that all wired objects are stored, or that all stored
objects are wired, or that neither of these statements is true. If only some
wired objects are stored, and only some stored objects are wired,
you will
be forced either to use multiple inheritance or to "hack around" the
problem. A potential "hack" for such a situation would be to inherit,
for example, Wired from Stored, and then for those
objects that are sent via modem, but are never stored, to make the
stored methods do nothing or return an error.
In fact, you realize that some stored objects
clearly are not wired: for example, user preferences. All wired objects,
however, are stored, and so your inheritance hierarchy so far is as reflected
in Figure 18.1.
Designing the Interfaces
It is important at this stage of designing your product to avoid being
concerned with implementation. You want to focus all of your energies on
designing a clean interface among the classes and then delineating what data
and methods each class will need.
It is often a good idea to have a solid understanding of the base
classes before trying to design the more derived classes, so you decide to
focus on pObject, pStored, and pWired.
The root class, pObject, will
only have the data and methods that are common to everything on
your system. Perhaps every object should have a unique identification
number. You could create pID (PostMaster ID) and make that a
member of pObject; but
first you must ask yourself, "Does any
object that is not stored and not wired need such a number?" That
begs the question, "Are there any objects that are not stored, but that
are part of this hierarchy?"
If there are no such objects, you may want to consider collapsing pObject and pStored into one
class; after all, if all objects are stored, what is the point of the
differentiation? Thinking this through,
you realize that there may be some objects, such as address objects,
that it would be beneficial to derive from pObject, but that will never be stored on their own; if they are stored, they
will be as
part of
some other object.
That says that for now having a separate pObject class would be useful. One can imagine that there will be an address
book that would be a collection of pAddress objects, and while no pAddress
will ever be stored on its own, there would be utility in having each
one have its own unique identification number. You tentatively assign pID to pObject, and
this means that pObject, at a
minimum,
will look like this:
class pObject
{
public:
~pObject();
pID GetID()const; void SetID(); private:
pID itsID;
}
There are a number of things to
note about this class declaration. First, this class is not declared to
derive from any other; this is your root class. Second, there is no
attempt to show implementation, even for methods such as GetID() that are likely to have inline implementation when you are done.
Third, const methods
are already identified; this is part of the interface, not the implementation.
Finally, a new data type is implied: pID.
Defining pID as a type, rather than using,
for example, unsigned long, puts
greater flexibility into your design.
If it turns out that you don't need an unsigned long, or that an unsigned long is not
sufficiently large, you can modify pID. That
modification will affect every place pID is used,
and you won't have to track down and edit every file with a pID in it.
For now, you will use typedef to
declare pID to be ULONG, which in turn you will declare to be unsigned long. This raises the question: Where do these declarations go?
When programming a large project, an overall design of the files is
needed. A standard approach, one which you will follow for this project, is
that each class appears in its own header file, and the
implementation
for the class methods appears in an associated CPP file. Thus, you will have a
file called OBJECT.HPP and
another called OBJECT.CPP. You
anticipate having other files such as MSG.HPP and MSG.CPP, with
the declaration of pMessage and the
implementation of its methods,
respectively.
NOTE: Buy it or write it? One question that you will confront
throughout the design phase of your program
is which routines might you buy and which must you write yourself. It is
entirely possible that you can take advantage of existing commercial libraries
to solve some or all of your communications issues. Licensing fees and other
non-technical concerns must also be resolved. It is often advantageous to
purchase such a library, and to focus your energies on your specific program,
rather than to "reinvent the wheel" about secondary technical issues.
You might even want to consider purchasing libraries that were not necessarily
intended for use with C++, if they can provide fundamental functionality you'd
otherwise have to engineer yourself. This can be instrumental in helping you
hit your deadlines.
Building a Prototype
For a project as large as PostMaster, it is unlikely that your initial
design will be complete and perfect. It would be easy to become overwhelmed by
the sheer scale of the problem, and trying to create all the classes and to complete
their interface before writing a line of working code is a recipe for disaster.
There are a number of good reasons to try out your design on a
prototype--a quick-and-dirty working example of your core ideas. There are a
number of different types of prototypes, however, each meeting different needs.
An interface design prototype provides the chance to test the look and
feel of your product with potential users.
A functionality prototype might be designed that
does not have the final user interface, but allows users to try out various
features, such as forwarding messages or attaching files without worrying about
the final interface.
Finally, an architecture prototype might be designed to give you a
chance to develop a smaller version of the program and to assess how easily
your design decisions will "scale up," as the program is fleshed out.
It is
imperative to keep your prototyping goals clear. Are you examining the user
interface, experimenting with functionality, or building a scale model of your
final product? A good architecture prototype makes a poor user interface
prototype, and vice versa.
It is also important to keep an eye on
over-engineering the prototype, or becoming so concerned with the investment
you've made in the prototype that you are reluctant to tear the code down and
redesign as you progress.
The 80/80 Rule
A good design rule of thumb at this stage is to design for those things
that 80 percent of the people want to do 80 percent of the time, and to set
aside your concerns about the remaining 20 percent. The "boundary
conditions" will need to be addressed sooner or later, but the core of
your design should focus on the 80/80.
In the face of this, you might decide to start by designing the
principal classes, setting aside the need for the secondary classes. Further,
when you identify multiple classes that will have similar designs with only
minor refinements, you might choose to pick one representative class and focus
on that, leaving until later the design and implementation of its close
cousins.
NOTE: There is another
rule, the 80/20 rule, which states that "the first 20% of your program will take 80% of your time to code, and the
remaining 80% of your program will take the other 80% of your time!"
In keeping with these considerations, you decide to focus on PostMasterMessage. This is the class that is most under your direct control.
As part
of its interface, PostMasterMessage will
need to talk with other types of messages, of course. You hope to be able to
work closely with the other message providers and to get their message format
specifications, but for now you can make some smart guesses just by observing
what is sent to your computer as you use their services.
In any case, you know that every PostMasterMessage will have a sender, a recipient, a date, and a subject, as well as the
body of the message and perhaps attached files. This tells you that you'll need
accessor methods for each of these attributes, as well as methods to report on
the size of the attached files, the size of the messages, and so forth.
Some of the services to which you will connect will use rich text--that
is, text with formatting instructions to set the font, character size, and
attributes, such as bold and italic. Other services do not support these
attributes, and those that do may or may not use their own proprietary scheme
for managing rich text. Your class will need conversion methods for turning
rich text into plain ASCII, and perhaps for turning other formats into
PostMaster formats.
Application Program Interface
An
Application Program Interface (API) is a set of documentation and routines for
using a service. Many of the mail providers will give you an API so that
PostMaster mail will be able to take advantage of their more advanced features,
such as rich text and embedding files. PostMaster will also want to publish its
own API so that other providers can plan for working with PostMaster in the
future.
Your PostMasterMessage class
will want to have a well-designed public interface, and the
conversion functions will be a principal component of PostMaster's API.
Listing 18.2 illustrates what PostMasterMessage's
interface looks like so far.
Listing 18.2. PostMasterMessages interface
class
PostMasterMessage : public MailMessage
{
public:
PostMasterMessage();
PostMasterMessage(
6: pAddress
Sender,
7: pAddress
Recipient,
8: pString
Subject,
10:
//
other constructors here
//
remember to include copy constructor
//
as well as constructor from storage
//
and constructor from wire format
//
Also include constructors from other formats
~PostMasterMessage();
pAddress&
GetSender() const;
void
SetSender(pAddress&);
//
other member accessors
20:
//
operator methods here, including operator equals
//
and conversion routines to turn PostMaster messages
//
into messages of other formats.
24:
private:
pAddress
itsSender;
pAddress
itsRecipient;
pString itsSubject;
pDate
itsCreationDate;
pDate
itsLastModDate;
pDate
itsReceiptDate;
pDate
itsFirstReadDate;
pDate
itsLastReadDate;
};
Output: None.
Analysis: Class PostMasterMessage is declared to derive from MailMessage. A number of constructors will be
provided, facilitating the creation of PostMasterMessages from other types
of mail
messages.
A number of accessor methods are anticipated for
reading and setting the various member data, as well as operators for turning
all or part of this message into other message formats. You anticipate storing
these messages to disk and reading them from the wire, so accessor methods are
needed for those purposes as well.
Programming in Large Groups
Even this preliminary architecture is enough to indicate how the various
development groups ought to proceed. The communications group can go ahead and
start work on the communications back end, negotiating a narrow interface with
the message format group.
The message format group will
probably lay out the general interface to the Message classes,
as was
begun above, and then will turn its attention to the question of how to
write data to the disk and read it back. Once this disk interface is well
understood, they will be in a good position to negotiate the interface to the
communications layer.
The message editors will be tempted to create
editors with an intimate knowledge of the internals of the Message class, but this would be a bad design mistake. They too must negotiate
a very narrow interface to the Message class;
message editor objects should know very little about the internal
structure
of messages.
Ongoing Design Considerations
As the project continues, you will repeatedly confront this basic design
issue: In which class should you put a given set of functionality (or
information)? Should the Message class
have this function, or should the Address class? Should the editor store this information, or should the message
store it
itself?
Your classes should operate on a "need to know" basis, much
like secret agents. They shouldn't share any more knowledge than is absolutely
necessary.
Design Decisions
As you
progress with your program, you will face hundreds of design issues. They will
range from the more global questions, "What do we want this to do?"
to the more specific, "How do we make this work?"
While the details of your implementation won't be finalized until you
ship the code, and some of the interfaces will continue to shift and change as
you work, you must ensure that your design is well understood early in the
process. It is imperative that you know what you are trying to build before you
write the code. The single most frequent cause of software dying on the vine
must be that there was not sufficient agreement early enough in the process
about what was being built.
Decisions, Decisions
To get a
feel for what the design process is like, examine this question, "What
will be on the menu?"
For PostMaster, the first choice is probably "new mail
message," and this immediately raises another design issue: When the user
presses New Message, what
happens? Does an editor get created,
which in turn creates a mail message, or does a new mail message get
created, which then creates the editor?
The command you are working with is "new mail message," so
creating a new mail message seems like the obvious thing to do. But what
happens if the user hits Cancel after starting to write the message? Perhaps it
would be cleaner to first create the editor and have it create (and own) the
new message.
The problem with this approach is that the editor will need to act
differently if it is creating a message than if it is editing the message,
whereas if the message is created first and then handed to the editor, only one
set of code need exist: Everything is an edit of an existing message.
If a message is created first, who creates it? Is it created by the menu
command code? If so, does the menu also tell the message to edit itself, or is
this part of the constructor method of the message?
It makes
sense for the constructor to do this at first glance; after all, every time you
create a message you'll probably want to edit it. Nonetheless, this is not a
good design idea. First, it is very possible that the premise is wrong: You may
well create "canned" messages (that is, error messages mailed to the
system operator) that are not put into an editor. Second, and more important, a
constructor's job is to create an object; it should do no more and no less than
that. Once a mail message is created, the constructor's job is done; adding a
call to the edit method just confuses the role of the constructor and makes the
mail message vulnerable to failures in the editor.
What is worse, the edit method will call another class, the editor,
causing its constructor to be called. Yet the editor is not a base class of the
message, nor is it contained within the message; it would be unfortunate if the
construction of the message depended on successful construction of the editor.
Finally, you won't want to call
the editor at all if the message can't be successfully created; yet
successful creation would, in this scenario, depend on calling the
editor! Clearly you want to fully return from the message's constructor before
calling Message::Edit().
DO look for objects that arise naturally out of your design. DO redesign as your understanding of the problem space
improves. DON'T share more
information among the classes than is absolutely necessary. DO look for opportunities to take
advantage of C++'s polymorphism.
Working with Driver Programs
One approach to surfacing design
issues is to create a driver program early in the process. For
example, the driver program for PostMaster might offer a very simple
menu, which will create PostMasterMessage objects,
manipulate them, and otherwise exercise some of the design.
New Term: A driver program is a function that exists only to demonstrate or test other functions.
Listing 18.3 illustrates a somewhat more robust definition of the PostMasterMessage class and a simple driver program.
#include
<iostream.h>
#include
<string.h>
typedef
unsigned long pDate;
enum
SERVICE
{ PostMaster, Interchange,
CompuServe, Prodigy, AOL, Internet };
class
String
{
public:
10:
|
//
constructors
|
11:
|
String();
|
12:
|
String(const
char *const);
|
13:
|
String(const
String &);
|
14:
|
~String();
|
15:
|
|
16:
|
//
overloaded operators
|
17:
|
char
& operator[](int offset);
|
18:
|
char
operator[](int offset) const;
|
19:
|
String
operator+(const String&);
|
20:
|
void
operator+=(const String&);
|
21:
|
String
& operator= (const String &);
|
22:
|
friend
ostream& operator<<
|
23:
|
(
ostream& theStream,String& theString);
|
24:
|
//
General accessors
|
25:
|
int
GetLen()const { return itsLen; }
|
26:
|
const char * GetString() const { return itsString; }
|
27:
|
//
static int ConstructorCount;
|
private:
29:
|
String
(int);
|
//
private constructor
|
30:
|
char
* itsString;
|
|
31:
|
unsigned short itsLen;
|
|
32:
|
||
33:
|
};
|
|
34:
|
//
default constructor creates string of 0 bytes
String::String()
{
itsString
= new char[1];
itsString[0]
= `\0';
itsLen=0;
//
cout << "\tDefault string constructor\n";
//
ConstructorCount++;
}
//
private (helper) constructor, used only by
//
class methods for creating a new string of
//
required size. Null filled.
String::String(int
len)
{
itsString
= new char[len+1];
for
(int i = 0; i<=len; i++)
52: itsString[1]
= `\0';
itsLen=len;
//
cout << "\tString(int) constructor\n";
//
ConstructorCount++;
}
57:
//
Converts a character array to a String
String::String(const
char * const cString)
{
itsLen
= strlen(cString);
itsString
= new char[itsLen+1];
for
(int i = 0; i<itsLen; i++)
64: itsString[i]
= cString[i];
itsString[itsLen]='\0';
//
cout << "\tString(char*) constructor\n";
//
ConstructorCount++;
}
69:
//
copy constructor
String::String
(const String & rhs)
{
itsLen=rhs.GetLen();
itsString
= new char[itsLen+1];
for
(int i = 0; i<itsLen;i++)
76: itsString[i]
= rhs[i];
itsString[itsLen]
= `\0';
//
cout << "\tString(String&) constructor\n";
//
ConstructorCount++;
}
81:
//
destructor, frees allocated memory
String::~String
()
{
delete
[] itsString;
itsLen
= 0;
//
cout << "\tString destructor\n";
}
89:
//
then copies string and size
String&
String::operator=(const String & rhs)
{
if
(this == &rhs)
95: return
*this;
delete
[] itsString;
itsLen=rhs.GetLen();
itsString
= new char[itsLen+1];
for
(int i = 0; i<itsLen;i++)
100: itsString[i]
= rhs[i];
itsString[itsLen]
= `\0';
return
*this;
//
cout << "\tString operator=\n";
}
105:
//non
constant offset operator, returns
//
reference to character so it can be
//
changed!
char
& String::operator[](int offset)
{
if
(offset > itsLen)
112: return
itsString[itsLen-1];
else
114: return
itsString[offset];
115: }
116:
//
constant offset operator for use
//
on const objects (see copy constructor!)
char
String::operator[](int offset) const
{
if
(offset > itsLen)
122: return
itsString[itsLen-1];
else
124: return
itsString[offset];
125: }
126:
//
creates a new string by adding current
//
string to rhs
String
String::operator+(const String& rhs)
{
int totalLen = itsLen + rhs.GetLen();
int
i,j;
String
temp(totalLen);
for
( i = 0; i<itsLen; i++)
135: temp[i]
= itsString[i];
137: temp[i]
= rhs[j];
temp[totalLen]='\0';
return
temp;
}
141:
void
String::operator+=(const String& rhs)
{
unsigned
short rhsLen = rhs.GetLen();
unsigned
short totalLen = itsLen + rhsLen;
String temp(totalLen);
for
(int i = 0; i<itsLen; i++)
148: temp[i]
= itsString[i];
for
(int j = 0; j<rhs.GetLen(); j++, i++)
150: temp[i]
= rhs[i-itsLen];
temp[totalLen]='\0';
*this
= temp;
}
154:
155: //
int String::ConstructorCount = 0;
156:
ostream&
operator<<( ostream& theStream,String& theString)
{
theStream
<< theString.GetString();
return
theStream;
}
162:
class
pAddress
{
public:
pAddress(SERVICE
theService,
167: const
String& theAddress,
168: const
String& theDisplay):
169: itsService(theService),
170: itsAddressString(theAddress),
171: itsDisplayString(theDisplay)
172: {}
//
pAddress(String, String);
//
pAddress();
//
pAddress (const pAddress&);
~pAddress(){}
friend ostream& operator<<(
ostream& theStream, pAddress& theAddress);
String&
GetDisplayString() { return itsDisplayString; }
private:
SERVICE
itsService;
String
itsDisplayString;
};
184:
ostream& operator<<(
ostream& theStream, pAddress& theAddress)
{
theStream
<< theAddress.GetDisplayString();
return
theStream;
}
190:
class
PostMasterMessage
{
public:
//
PostMasterMessage();
PostMasterMessage(const
pAddress& Sender,
197: const pAddress& Recipient, 198: const String&
Subject,
199: const
pDate& creationDate);
200:
//
other constructors here
//
remember to include copy constructor
//
as well as constructor from storage
//
and constructor from wire format
//
Also include constructors from other formats
~PostMasterMessage(){}
207:
208: void
Edit(); // invokes editor on this message
209:
pAddress&
GetSender() const { return itsSender; }
pAddress&
GetRecipient() const { return itsRecipient; }
String&
GetSubject() const { return itsSubject; }
// void SetSender(pAddress& );
//
other member accessors
215:
//
operator methods here, including operator equals
//
and conversion routines to turn PostMaster messages
//
into messages of other formats.
219:
private:
pAddress
itsSender;
pAddress
itsRecipient;
String itsSubject;
pDate
itsCreationDate;
pDate
itsLastModDate;
pDate
itsFirstReadDate;
pDate
itsLastReadDate;
};
230:
PostMasterMessage::PostMasterMessage(
232: const pAddress& Sender, 233: const pAddress&
Recipient, 234: const String& Subject, 235: const pDate& creationDate):
236: itsSender(Sender),
237: itsRecipient(Recipient), 238: itsSubject(Subject),
239: itsCreationDate(creationDate), 240:
itsLastModDate(creationDate),
241: itsFirstReadDate(0),
242: itsLastReadDate(0)
{
cout
<< "Post Master Message created. \n";
}
246:
void
PostMasterMessage::Edit()
{
249: cout
<< "PostMasterMessage edit function called\n";
250: }
251:
252:
int
main()
{
pAddress
Sender(PostMaster, "jliberty@PostMaster", "Jesse
Liberty");
pAddress
Recipient(PostMaster, "sl@PostMaster","Stacey
Liberty");
PostMasterMessage PostMessage(Sender,
Recipient, "Saying Hello", 0);
cout
<< "Message review... \n";
cout
<< "From:\t\t" << PostMessage.GetSender() << endl;
cout
<< "To:\t\t" << PostMessage.GetRecipient() << endl;
cout
<< "Subject:\t" << PostMessage.GetSubject() <<
endl;
return
0;
}
WARNING: If you receive a "can't convert" error, remove the const keywords from lines 210-212.
Message review...
From: Jesse
Liberty
To: Stacey
Liberty
Subject: Saying
Hello
Analysis: On line 4, pDate is type-defined to be an unsigned long. It is not uncommon for
dates to be stored as a long integer
(typically as the number of seconds since an arbitrary starting date such as January
1, 1900). In this program, this is a
placeholder; you would expect to eventually turn pDate into a real class.
On line 5, an enumerated constant, SERVICE, is
defined to allow the Address objects
to keep track of what type of address they are, including PostMaster,
CompuServe, and so forth.
Lines 7-161 represent the interface to and implementation of String, along much the same lines as you have seen in previous chapters. The String class is used for a number of member variables in all of the Message classes and various other classes used by messages, and as such it is
pivotal in your program. A full and robust String class will be essential to making your Message classes
complete.
On lines 162-183, the pAddress class is
declared. This represents only the fundamental functionality of this class, and
you would expect to flesh this out once your program is better
understood. These objects represent essential components in every
message: both the sender's address and that of the recipient. A fully
functional pAddress object
will be able to handle forwarding
messages,
replies, and so forth.
It is the
pAddress object's
job to keep track of the display string as well as the internal routing string
for its service. One open question for your design is whether there should be
one pAddress object
or
if this should be subclassed for each service type. For now, the service
is tracked as an enumerated constant, which is a member variable of each pAddress object.
Lines 191-229 show the interface
to the PostMasterMessage class. In this particular listing, this
class stands on its own, but very soon you'll want to make this part of
its inheritance hierarchy. When you do redesign this to inherit from Message, some of the member variables may move into the base
classes,
and some of the member functions may become overrides of base class methods.
A variety of other constructors, accessor functions, and other member
functions will be required to make this class fully functional. Note that what
this listing illustrates is that your class does not have to be 100 percent
complete before you can write a simple driver program to test some of your
assumptions.
On lines 247-250, the Edit() function
is "stubbed out" in just enough detail to indicate where the editing
functionality will be put once this class is fully operational.
Lines 253-263 represent the
driver program. Currently this program does nothing more than exercise a few of
the accessor functions and the operator<<
overload. Nonetheless, this gives you the starting point for experimenting with
PostMasterMessages and a
framework within which you can
modify
these classes and examine the impact.
Summary
Today you saw a review of how to bring together many of the elements of
C++ syntax and apply them to object-oriented analysis, design, and programming.
The development cycle is not a linear progression from clean analysis through
design and culminating in programming; rather, it is cyclical. The first phase
is typically analysis of the problem, with the results of that analysis forming
the basis for the preliminary design.
Once a
preliminary design is complete, programming can begin, but the lessons learned
during the programming phase are fed back into the analysis and design. As
programming progresses, testing and then debugging begins. The cycle continues,
never really ending; although discrete points are reached, at which time it is
appropriate to ship the product.
When analyzing a large problem from an object-oriented viewpoint, the
interacting parts of the problem are often the objects of the preliminary
design. The designer keeps an eye out for process, hoping to encapsulate
discrete activities into objects whenever possible.
A class hierarchy must be designed, and fundamental
relationships among the interacting parts must be established. The preliminary
design is not meant to be final, and functionality will migrate among objects
as the design solidifies.
It is a principal goal of object-oriented analysis to hide as much of
the data and implementation as possible and to build discrete objects that have
a narrow and well-defined interface. The clients of your object should not need
to understand the implementation details of how they fulfill their
responsibilities.
Q&A
In what way is object-oriented analysis and design fundamentally
different from other approaches?
Prior to the development of these
object-oriented techniques, analysts and programmers tended to think of
programs as functions that acted on data. Object-oriented programming focuses
on the integrated data and functionality as discrete units that have both
knowledge (data) and capabilities (functions). Procedural programs, on the
other hand, focus on functions and how they act on data. It has been said that
Pascal and C programs are collections of procedures and C++ programs are
collections of classes.
Is object-oriented programming finally the silver bullet that will solve
all programming problems?
No, it was never intended to be.
For large, complex problems, however, object-oriented analysis, design, and
programming can provide the programmer with tools to manage enormous complexity
in ways that were previously impossible.
Is C++
the perfect object-oriented language?
C++ has a number of advantages
and disadvantages when compared with alternative object-oriented programming
languages, but it has one killer advantage above and beyond all others: It is
the single most popular object-oriented programming language on the face of the
Earth. Frankly, most programmers don't decide to program in C++ after an
exhaustive analysis of the alternative object-oriented programming languages;
they go where the action is, and in the 1990s the action is with C++. There are
good reasons for that; C++ has a lot to offer, but this book exists, and I'd
wager you are reading it, because C++ is the development language of choice at
so many corporations.
Where can
I learn more about object-oriented analysis and design?
Day 21 offers some further
suggestions, but it is my personal opinion that there are a number of terrific
object-oriented analysis and design books available. My personal favorites
include:
Object-Oriented Analysis and Design with Applications by Grady Booch
(2nd Edition). Published by Benjamin/Cummings Publishing Company, Inc., ISBN:
0-8053-5340-2. Object-Oriented Modeling and Design by Rumbaugh, Blaha,
Premerlani, Eddy, and Lorenson. Published by Prentice-Hall, ISBN 0-13-629841-9.
There are many other excellent alternatives. Also
be sure to join one of the newsgroups or conferences on the Internet,
Interchange, or one of the alternative dial-up services.
Workshop
The Workshop provides quiz questions to help you solidify your
understanding of the material covered and exercises to provide you with
experience in using what you've learned. Try to answer the quiz and exercise
questions before checking the answers in Appendix D, and make sure you understand
the answers before continuing to the next chapter.
Quiz
What is the difference between
object-oriented programming and procedural programming?
To what does
"event-driven" refer?
What are the stages in the
development cycle?
What is a rooted hierarchy?
What is a driver program?
Exercises
Suppose you had to simulate the
intersection of Massachusetts Avenue and Vassar Street-- two typical two-lane
roads, with traffic lights and crosswalks. The purpose of the simulation is to
determine if the timing of the traffic signal allows for a smooth flow of
traffic.
What kinds of objects should be modeled in the
simulation? What would the classes be for the simulation?
Suppose the intersection from
Exercise 1 were in a suburb of Boston, which has arguably the unfriendliest
streets in the United States. At any time there are three kinds of Boston
drivers:
Locals,
who continue to drive through intersections after the light turns red;
tourists, who drive slowly and cautiously (in a rental car, typically); and
taxis, who have a wide variation of driving patterns, depending on the kinds of
passengers in the cabs.
Also, Boston has two kinds of pedestrians: locals, who cross the street
whenever they feel like it and seldom use the crosswalk buttons; and tourists,
who always use the crosswalk buttons and only cross when the Walk/Don't Walk
light permits.
Finally, Boston has bicyclists who never pay
attention to stop lights. How do these considerations change the model?
You are asked to design a group
scheduler. The software allows you to arrange meetings among individuals or
groups and to reserve a limited number of conference rooms. Identify the
principal subsystems.
Design and show the interfaces to
the classes in the room reservation portion of the program discussed in
Exercise 3.
0 comments:
Post a Comment