Previous Section
 < Free Open Study > 
Next Section


3.6 Overloading Operators

In this chapter, we have looked at a general implementation of two abstract data types that represent what we normally think of as lists. We have used a sequential array-based implementation where the user is responsible for providing the description of the items on the list. We required the user to provide a comparison operation called ComparedTo for comparing items on the list. The technique presented here works in most programming languages. In this section, however, we cover a C++ construct that makes comparisons simpler and more robust. C++ allows us to redefine the meaning of the relational operators in terms of the data members of a class. This redefining of an operator symbol is called overloading the operator.

Let's describe the mechanism in terms of the StrType class we defined in Chapter 2. As an exercise, you were asked to add a comparison operation to this class. We also need this operation in our Case Study. The expanded definition of the class StrType is shown here:

enum InType (ALPHA_NUM, ALPHA, NON_WHITE, NOT_NEW};
const int MAX = 200;

class StrType
{
public:
  StrType();
  void GetString(bool skip, InType inString);
  void GetStringFile(bool skip, InType inString,
    std::ifstream&  inFile);
  void PrintToScreen(bool newLine);
  void PrintToFile(bool newLine, std::ofstream& outFile);
  bool operator<(StrType otherString) const;
  bool operator==(StrType otherString) const;
private:
  char letters[MAX+1];
};

The syntax for overloading a symbol is the reserved word operator followed by the symbol to be overloaded. These member functions are known in C++ as operator functions. Because we store the characters in the same way that C++ does, we can use the string functions provided in <cstring> to implement these operator functions.

bool StrType::operator<(StrType otherString) const
{
  int result;

  result = std::strcmp(letters, otherString.letters);
  if (result < 0)
    return true;
  else
    return false;
}

bool StrType::operator==(StrType otherString) const
{
  int result;

  result = std::strcmp(letters, otherString.letters);
  if (result == 0)
    return true;
  else
    return false;
}

When the client code includes

if (myString < yourString)

or

if (myString == yourString)

the respective member functions from StrType are invoked.

For our Unsorted ADT and Sorted ADT, we required ItemType to be a class with a member function ComparedTo. Now that we know how to overload the relational operators, we could overload "<" and "= =" in the ItemType class and then rewrite the code for InsertItem, RetrieveItem, and DeleteItem using the relational operators. We could-but should we? We cannot use the relational operators as labels on a switch statement, so the code would have to be a series of if-else clauses. Some programmers find switch statements more self-documenting, whereas others like to use the relational operators. The choice is a matter of personal style.

3.7 Object-Oriented Design Methodology

The object-oriented design that we present here involves four stages. Brainstorming is the stage in which we make a first pass at determining the classes in the problem. Filtering is the stage in which we go back over the proposed classes determined in the brainstorming stage to see whether any can be combined, any are not needed, or any are missing. Each class that survives the filtering stage is recorded on a CRC card.

Scenarios is the stage in which we determine the behavior of each class. Because each class is responsible for its own behavior, we call the behaviors responsibilities. In this stage, we explore "what if" questions to ensure that all situations are examined. When we have determined all of the responsibilities of each class, we record them on the class's CRC card, along with the names of any other classes with which that class must collaborate (interact) to complete its responsibility.

In Responsibility Algorithms, the last stage, we write the algorithms for each of the responsibilities outlined on the CRC cards. Now you can see the source of the term CRC: Class, Responsibility, and Collaboration.

Although these techniques are designed to be applied in groups, we can apply them to our own individual thought processes as well, so let's look at each of these stages in a little more detail.

Brainstorming

Exactly what is brainstorming? The dictionary defines it as a group problem-solving technique that involves the spontaneous contribution of ideas from all members of the group.[1] Brainstorming brings to mind a movie or TV show where a group of bright young people toss around ideas about an advertising slogan for the latest revolutionary product. This picture seems at odds with the traditional picture of a computer analyst working alone in a closed, windowless office for days who finally jumps up shouting "Ah ha!" As computers have become more powerful, the problems that can be solved have grown ever more complex, and the picture of the genius locked in a windowless room has become obsolete. Solutions to complex problems need new and innovative solutions based on collective "Ah ha!"s.

Belin and Simone list four principles of successful brainstorming.[2] First and foremost, all ideas are potential good ideas. It is imperative that the members of the group not censor their own ideas or make judgments out of hand on other's ideas. The second principle relates to pace: Think fast and furiously first, and ponder later. The faster the initial pace, the better the creative juices will flow. Third, give every voice a turn. To slow down those predisposed to hog the conversation and spur those reluctant to talk, use a rotation. Continue this pattern until team members are truly forced to "pass" because they are out of ideas. Fourth, a little humor can be a powerful force. Humor helps convert a random group into a cohesive team.

In the context of object-oriented problem solving, brainstorming is a group activity designed to produce a list of candidate classes to be used to solve a particular problem. As Belin and Simone point out, although each project is different and each team has a different personality, following the four steps described next is a good general approach.

Step 1 is to review brainstorming principles at the beginning of the meeting to remind everyone that the exercise is a group activity and that personal style should therefore be put aside. Step 2 is to state specific session objectives such as "Today we want to come up with a list of candidate classes for the student project" or "Today we want to determine the classes that are active during the registration phase." Step 3 is to use a round-robin technique to allow the group to proceed at an even tempo but give people enough time to think. Each person should contribute a possible object class to the list. A facilitator should keep the discussion on target, and a scribe should take notes. The brainstorming stops when each person in the group has to "pass" because he or she cannot think of another class to suggest. Step 4 of Belin and Simone's process is to discuss the classes and select the final list of classes. We prefer to think of this stage as separate from brainstorming and so discuss it in the section on filtering.

Just as the people brainstorming for an advertising slogan know something about the product before the session begins, brainstorming for classes requires that the participants know something about the problem at hand. Each participant should be familiar with the requirements document and any correspondence relating to the technical aspects of the project. If ambiguities are identified, participants should conduct interviews to clarify these points before the brainstorming sessions commence. Each team member should enter the brainstorming sessions with a clear understanding of the problem to be solved. During the preparation, each team member will almost certainly have generated his or her own preliminary list of classes.

Filtering

As noted previously, the brainstorming session produces a tentative list of classes. In the filtering phase, the group takes the tentative list of classes and determines which should serve as the core classes in the problem solution. The list may contain two classes that are actually the same thing. These duplicate classes usually arise because people working in different parts of an organization use different names for the same concept or entity. Perhaps two classes in the list share many attributes and behaviors. The common parts should be gathered together into a superclass in which the two classes derive the common properties and add the properties that are different.

Some classes may not really belong in the problem solution. For example, if we are simulating a calculator, we might list the user as a possible class. In fact, the user is not a class within the simulation; the user is an entity outside the problem who provides input to the simulation. Another possible class might be the on button. A little thought shows that the on button is not really part of the simulation; it simply starts the simulation program running.

As the filtering is completed, CRC cards should be written for each class that has survived to this stage.

Scenarios

In this phase, we assign responsibilities to each class. What are responsibilities? They are the tasks that each class must perform. Responsibilities are eventually implemented as subprograms. At this stage we are interested only in what the tasks are, not in how they might be carried out.

Two types of responsibilities exist: what a class must know about itself (knowledge) and what a class must be able to do (behavior). A class encapsulates its data (knowledge); objects in one class cannot directly access data in another class. Encapsulation is a key to abstraction. However, each class has the responsibility of making data (knowledge) available to other classes that need it. Therefore, each class has the responsibility for knowing the things about itself that others need to know. For example, a student class should "know" its name and address. The responsibilities for this knowledge might be called and . Whether the address is kept in the student class or whether the student class must ask some other class to access the address is irrelevant at this stage. The important fact is that the student class knows its own address.

The responsibilities for behavior look more like the tasks we described in top-down design. For example, a responsibility for the student class might be to calculate its GPA (grade point average). In top-down design, we would say that a task is to calculate the GPA given the data. In object-oriented design, we would say that the student class is responsible for calculating its own GPA. The distinction here is both subtle and profound. The final code for the calculation may look the same, but it is executed in different ways. In an imperative program, the program calls a subprogram that calculates the GPA, passing the student object as a parameter. In an object-oriented program, a message is sent to the object of the class to calculate its GPA. There are no parameters because the object to which the message is sent knows its own data.

The name for the scenarios phase gives a clue as to how we go about assigning responsibilities to classes. The team uses play-acting to test different scenarios. Each member of the team plays the role of one of the classes. Scenarios are "what if" scripts that allow participants to act out different situations. When a class has been sent a message, the actor holds up the CRC card and responds to the message by sending messages to other parties as needed. As the scripts are acted out, missing responsibilities are unearthed and unneeded responsibilities are detected. Sometimes the need for new classes surfaces. Although waving cards in the air when "you" are active may seem a little embarrassing at first, team members quickly get into the spirit of the action when they see how effective the technique is. See Figure 3.11.

Click To expand
Figure 3.11: A scenario walk-through in progress

The output from this phase is a set of CRC cards representing the core classes in the problem solution. Each card lists the responsibilities for each class, along with the classes with which a responsibility must collaborate.

Responsibility Algorithms

Eventually, we must write the algorithms for the responsibilities. Because the design process focuses on data rather than actions in the object-oriented view of design, the algorithms for carrying out responsibilities tend to be fairly short. For example, the knowledge responsibilities usually return the contents of just one of an object's variables, or send a message to another object to retrieve it. Action responsibilities are a little more complicated, often involving calculations. For this reason, the top-down method of designing an algorithm is usually appropriate for designing many responsibility algorithms.

A Final Word

To summarize, top-down design methods focus on the process of transforming the input into the output, resulting in a hierarchy of tasks. Object-oriented design focuses on the data objects that will be transformed, resulting in a hierarchy of objects. The nouns in the problem description become objects; the verbs become operations. In a top-down design, the verbs are the primary focus; in an object-oriented design, the nouns are the primary focus.

[1]Webster's New Collegiate Dictionary.

[2]D. Belin and S. S. Simone, The CRC Card Book (Reading, MA: Addison-Wesley, 1997).



Previous Section
 < Free Open Study > 
Next Section
Converted from CHM to HTML with chm2web Pro 2.85 (unicode)