Site hosted by Angelfire.com: Build your free website today!

<< Back Index Next >>

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

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:
5:     class Condition
6:     {
7:     public:
8:        Condition() { }
9:        virtual ~Condition() {}
10:       virtual void Log() = 0;
11:    };
12:
13:    class Normal : public Condition
14:    {
15:    public:
16:       Normal() { Log(); }
17:       virtual ~Normal() {}
18:       virtual void Log() { cout << "Logging normal conditions...\n"; }
19:    };
20:
21:    class Error : public Condition
22:    {
23:    public:
24:       Error() {Log();}
25:       virtual ~Error() {}
26:       virtual void Log() { cout << "Logging error!\n"; }
27:    };
28:
29:    class Alarm : public Condition
30:    {
31:    public:
32:       Alarm ();
33:       virtual   ~Alarm() {}
34:       virtual void Warn() { cout << "Warning!\n"; }
35:       virtual void Log() { cout << "General Alarm log\n"; }
36:       virtual void Call() = 0;
37:
38:    };
39:
40:    Alarm::Alarm()
41:    {
42:        Log();
43:       Warn();
44:    }
45:    class FireAlarm : public Alarm
46:    {
47:    public:
48:       FireAlarm(){Log();};
49:       virtual ~FireAlarm() {}
50:       virtual void Call() { cout<< "Calling Fire Dept.!\n"; }
51:       virtual void Log() { cout << "Logging fire call.\n"; }
52:    };
53:
54:    int main()
55:    {
56:       int input;
57:       int okay = 1;
58:       Condition * pCondition;
59:       while (okay)
60:       {
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;
74:                break;
75:             default:
76:                pCondition = new Error;
77:                delete pCondition;
78:                okay = 0;
79:                break;
80:          }
81:       }
82:     return 0;
83: }

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:

1. Communications: the ability for the software to dial into the e-mail provider via modem, or to connect over a network.

2. Database: the ability to store data and to retrieve it from disk.

3. E-mail: the ability to read various e-mail formats and to write new messages to each system.

4. Editing: providing state-of-the-art editors for the creation and manipulation of messages.

5. Platform issues: the various UI issues presented by each platform (DOS, Macintosh, and so on).

6. Extensibility: planning for growth and enhancements.

7. 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:

1. Communications: responsible for both dial-up and network communications. They deal with packets, streams, and bits, rather than with e-mail messages per se.

2. 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.

3. 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.

Initial Class Design

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?

Rooted Hierarchies Versus Non-Rooted Hierarchies

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 in service of adding the ability to be transferred over the modem.

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.

Figure 18.1. Initial inheritance hierarchy.

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();
~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!"

Designing the PostMasterMessage Class

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

1:     class PostMasterMessage : public MailMessage
2:     {
3:     public:
4:       PostMasterMessage();
5:       PostMasterMessage(
6:           pAddress Sender,
7:           pAddress Recipient,
8:           pString Subject,
9:           pDate creationDate);
10:
11:      // other constructors here
12:      // remember to include copy constructor
13:      // as well as constructor from storage
14:      // and constructor from wire format
15:      // Also include constructors from other formats
16:      ~PostMasterMessage();
17:      pAddress& GetSender() const;
18:      void SetSender(pAddress&);
19:      // other member accessors
20:
21:      // operator methods here, including operator equals
22:      // and conversion routines to turn PostMaster messages
23:      // into messages of other formats.
24:
25:    private:
26:       pAddress itsSender;
27:       pAddress itsRecipient;
28:       pString  itsSubject;
29:       pDate itsCreationDate;
30:       pDate itsLastModDate;
31:       pDate itsReceiptDate;
32:       pDate itsFirstReadDate;
33:       pDate itsLastReadDate;
34: };


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.

Listing 18.3. A driver program for PostMasterMessage.

1:     #include <iostream.h>
2:     #include <string.h>
3:
4:     typedef unsigned long pDate;
5:     enum SERVICE 
6:       { PostMaster, Interchange, CompuServe, Prodigy, AOL, Internet };
7:     class String
8:     {
9:        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;
28:       private:
29:          String (int);         // private constructor
30:          char * itsString;
31:          unsigned short itsLen;
32:
33:    };
34:
35:    // default constructor creates string of 0 bytes
36:    String::String()
37:    {
38:       itsString = new char[1];
39:       itsString[0] = `\0';
40:       itsLen=0;
41:       // cout << "\tDefault string constructor\n";
42:       // ConstructorCount++;
43:    }
44:
45:    // private (helper) constructor, used only by
46:    // class methods for creating a new string of
47:    // required size.  Null filled.
48:    String::String(int len)
49:    {
50:       itsString = new char[len+1];
51:       for (int i = 0; i<=len; i++)
52:          itsString[1] = `\0';
53:       itsLen=len;
54:       // cout << "\tString(int) constructor\n";
55:       // ConstructorCount++;
56:    }
57:
58:    // Converts a character array to a String
59:    String::String(const char * const cString)
60:    {
61:       itsLen = strlen(cString);
62:       itsString = new char[itsLen+1];
63:       for (int i = 0; i<itsLen; i++)
64:          itsString[i] = cString[i];
65:       itsString[itsLen]='\0';
66:       // cout << "\tString(char*) constructor\n";
67:       // ConstructorCount++;
68:    }
69:
70:    // copy constructor
71:    String::String (const String & rhs)
72:    {
73:       itsLen=rhs.GetLen();
74:       itsString = new char[itsLen+1];
75:       for (int i = 0; i<itsLen;i++)
76:          itsString[i] = rhs[i];
77:       itsString[itsLen] = `\0';
78:       // cout << "\tString(String&) constructor\n";
79:       // ConstructorCount++;
80:    }
81:
82:    // destructor, frees allocated memory
83:    String::~String ()
84:    {
85:       delete [] itsString;
86:       itsLen = 0;
87:       // cout << "\tString destructor\n";
88:    }
89:
90:    // operator equals, frees existing memory
91:    // then copies string and size
92:    String& String::operator=(const String & rhs)
93:    {
94:       if (this == &rhs)
95:          return *this;
96:       delete [] itsString;
97:       itsLen=rhs.GetLen();
98:       itsString = new char[itsLen+1];
99:       for (int i = 0; i<itsLen;i++)
100:         itsString[i] = rhs[i];
101:      itsString[itsLen] = `\0';
102:      return *this;
103:      // cout << "\tString operator=\n";
104:   }
105:
106:   //non constant offset operator, returns
107:   // reference to character so it can be
108:   // changed!
109:   char & String::operator[](int offset)
110:   {
111:      if (offset > itsLen)
112:         return itsString[itsLen-1];
113:      else
114:         return itsString[offset];
115:   }
116:
117:   // constant offset operator for use
118:   // on const objects (see copy constructor!)
119:   char String::operator[](int offset) const
120:   {
121:      if (offset > itsLen)
122:         return itsString[itsLen-1];
123:      else
124:         return itsString[offset];
125:   }
126:
127:   // creates a new string by adding current
128:   // string to rhs
129:   String String::operator+(const String& rhs)
130:   {
131:      int  totalLen = itsLen + rhs.GetLen();
132:      int i,j;
133:      String temp(totalLen);
134:      for ( i = 0; i<itsLen; i++)
135:         temp[i] = itsString[i];
136:      for ( j = 0; j<rhs.GetLen(); j++, i++)
137:         temp[i] = rhs[j];
138:      temp[totalLen]='\0';
139:      return temp;
140:   }
141:   
142:   void String::operator+=(const String& rhs)
143:   {
144:      unsigned short rhsLen = rhs.GetLen();
145:      unsigned short totalLen = itsLen + rhsLen;
146:      String  temp(totalLen);
147:      for (int i = 0; i<itsLen; i++)
148:         temp[i] = itsString[i];
149:      for (int j = 0; j<rhs.GetLen(); j++, i++)
150:         temp[i] = rhs[i-itsLen];
151:      temp[totalLen]='\0';
152:      *this = temp;
153:   }
154:
155:   // int String::ConstructorCount = 0;
156:
157:   ostream& operator<<( ostream& theStream,String& theString)
158:   {
159:       theStream << theString.GetString();
160:       return theStream;
161:   }
162:
163:   class pAddress
164:   {
165:   public:
166:      pAddress(SERVICE theService,
167:               const String& theAddress,
168:               const String& theDisplay):
169:         itsService(theService),
170:         itsAddressString(theAddress),
171:         itsDisplayString(theDisplay)
172:         {}
173:      // pAddress(String, String);
174:      // pAddress();
175:      // pAddress (const pAddress&);
176:      ~pAddress(){}
177:      friend ostream& operator<<( ostream& theStream, pAddress& theAddress);
178:      String& GetDisplayString() { return itsDisplayString; }
179:   private:
180:      SERVICE itsService;
181:      String itsAddressString;
182:      String itsDisplayString;
183:   };
184:
185:   ostream& operator<<( ostream& theStream, pAddress& theAddress)
186:   {
187:       theStream << theAddress.GetDisplayString();
188:       return theStream;
189:   }
190:
191:   class PostMasterMessage
192:   {
193:   public:
194:     // PostMasterMessage();
195:
196:     PostMasterMessage(const pAddress& Sender,
197:                     const pAddress& Recipient,
198:                     const String& Subject,
199:                     const pDate& creationDate);
200:
201:     // other constructors here
202:     // remember to include copy constructor
203:     // as well as constructor from storage
204:     // and constructor from wire format
205:     // Also include constructors from other formats
206:     ~PostMasterMessage(){}
207:
208:     void Edit(); // invokes editor on this message
209:
210:     pAddress& GetSender() const { return itsSender; }
211:     pAddress& GetRecipient() const { return itsRecipient; }
212:     String& GetSubject() const { return itsSubject; }
213:     //  void SetSender(pAddress& );
214:     // other member accessors
215:
216:     // operator methods here, including operator equals
217:     // and conversion routines to turn PostMaster messages
218:     // into messages of other formats.
219:
220:   private:
221:      pAddress itsSender;
222:      pAddress itsRecipient;
223:      String  itsSubject;
224:      pDate itsCreationDate;
225:      pDate itsLastModDate;
226:      pDate itsReceiptDate;
227:      pDate itsFirstReadDate;
228:      pDate itsLastReadDate;
229:   };
230:
231:   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)
243:     {
244:       cout << "Post Master Message created. \n";
245:     }
246:
247:     void PostMasterMessage::Edit()
248:      {
249:         cout << "PostMasterMessage edit function called\n";
250:      }
251:
252:
253:   int main()
254:   {
255:      pAddress Sender(PostMaster, "jliberty@PostMaster", "Jesse Liberty");
256:      pAddress Recipient(PostMaster, "sl@PostMaster","Stacey Liberty");
257:      PostMasterMessage PostMessage(Sender, Recipient, "Saying Hello", 0);
258:      cout << "Message review... \n";
259:      cout << "From:\t\t" << PostMessage.GetSender() << endl;
260:      cout << "To:\t\t" << PostMessage.GetRecipient() << endl;
261:      cout << "Subject:\t" << PostMessage.GetSubject() << endl;
262:     return 0;
263: }


WARNING: If you receive a "can't convert" error, remove the const keywords from lines 210-212.
Output: Post Master Message created.
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

Q. In what way is object-oriented analysis and design fundamentally different from other approaches?

A.
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.

Q. Is object-oriented programming finally the silver bullet that will solve all programming problems?

A. 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.

Q. Is C++ the perfect object-oriented language?

A. 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.

Q. Where can I learn more about object-oriented analysis and design?

A. 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

1. What is the difference between object-oriented programming and procedural programming?

2. To what does "event-driven" refer?

3. What are the stages in the development cycle?

4. What is a rooted hierarchy?

5. What is a driver program?

6. W
hat is encapsulation?

Exercises

1. 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?

2. 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?

3. 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.

4. Design and show the interfaces to the classes in the room reservation portion of the program discussed in Exercise 3.


<< Back Index Next >>