Good question! How?
Software engineering has been concerned with this question
for a long time and a number of different processes
have been proposed and used to try to meet the problems
associated with developing software to be used some set of
clients (that is, not just by the developer herself or
himself).
What problems arise?
-
A client (maybe your instructor) tells you what he wants. You
write a program to do it. It takes a long time, but you finally
finish. You deliver it to the client. The client says
"Oh. Well now that I see how the program works, I would really
rather have it do .... instead. Can you fix that by tomorrow for
the big meeting?
-
A client describes what he wants. You write up his
requirements and get him to sign a requirements
document which says that he will accept a system that meets
those requirements!
Next you start writing code for the system. But after
about a week each new part of the system you write causes
earlier working parts of the system to fail. Uh oh! How are
you going to meet the promised delivery date?
Software engineering processes (if they are any good) should
help to direct larger system development to help solve these and
other such problems, independently of the actual algorithms and
data needed in any specific system.
These steps are generally needed in any moderate to large
scale system development and can be a useful guide even in small
systems consisting of a single program.
- Analysis
- Design
- Implementation
- Testing
- Deployment
In this phase it is important for the developer and the
client to agree on the requirements of the software system to be
built.
This can be quite difficult as the client may not really know
what is possible and what isn't or what he really needs.
The output of the analysis phase should be a detailed
requirements document.
This is the phase that the developer discovers the structures
needed to develop a system to meet the requirements from the
analysis phase.
The design phase should not commit to early to writing
code. However, in the case of an object oriented system, the
output of the design phase should be a set of classes
with the member functions declared and comments
describing what they should do. These classes with
their operations (member functions) should be sufficient to
develop a system that meets the requirements.
The implementation of the functions is not specified
and data members of classes are not required yet. So it is
important that the comment for each member function clearly
states what the function must do (but not how).
Member function stubs can be written that include the parameters and
return type. However, the bodies will be left blank or return a
default value in place of the actual implementation that comes
in a later phase.
In this phase, you finally begin actual coding. But the
design phase should have provided sufficient organization
so that the parts will fit together as long as each class member
function is implemented to fulfill its responsibility as
specified in the comment generated in the design phase.
Each class should be "unit" tested. That is, test programs
should be written to make sure each member function does what it
should. If class A depends on class B, it is a good idea to make
sure class B is unit tested first, then test class A. Otherwise,
class A may fail tests simply because class B is not implemented
correctly.
If the software is going to be delivered to mulitple clients
or if a client uses mulitple hardware platforms, some thought
must be given to how the software will be delivered and
installed on the client's machine(s). A number of long standing
practices and tools have been developed for this purpose, but it
is still quite challenging. Does a client exclusively use
Windows and Microsoft environment? Linux?
In addition, relationships between the set of classes should
be made clear in the design phase. That is, some classes may
need to use some of the other classes.
One class can use another class in several different
ways:
In these examples the relationship is that one class uses
another class by having a member of that class type.
This is called the has-a relation ship. One class has
a member whose type is another class.
A PointSet has Points. The PointSet class will have a member
(an array) to hold a collections of Points.
A Book has Chapters and an Index. A Book class might
similarly, have a member that is an array of Chapter elements
and another member of type Index.
The term aggregation is also used. For example, the class Book
aggregates or has members of type Preface,
Chapter, Appendix, and Index.
The Point class has a member function toString()
string Point::toString()
{
string result;
....
return string;
}
This function returns a string such as "(3,4)" representation
of the Point value with member x value 3 and member y value
4.
So the Point class uses the string class, but the Point class
does not have a string data member.
The relationship between Point and string is not
aggregation. It is a weaker relationship, but the Point
class does depend on the string class.
This dependency relationship will simply be referred to as
"depends-on" or "uses".
One class can be an extension of another class. It has all
the properties and member functions of the other class, but may
add a few more. This class relationship is called
inheritance and will be covered in later lectures.
For now the question is given a problem or system to build,
how do we design a solution. In particular, what can guide us in
choosing what classes should be created and used to solve the
problem.
Only the design details will be discussed in
detail.
So we will consider requirements for a system will be given
and the problem is to decide on a set of classes that will be
needed to build such a system.
Is there some guide how to use the requirements to
discover what classes might be appropriate?
Caution: Initially, when building classes, it may
frequently occur that you realize you need another member
function in the class. After some practice in building classes,
there may be a tendency to compensate for this by including a number
of member functions that you really don't need for this
system. It isn't clear when to stop adding member functions for
possible future systems. Remember the client who immediately
says "Great! But can you add this other feature." (A feature
that wasn't in the requirements.) Adding
member functions that seem match the class type, indepently of
whether they are actually needed for this system, can make the
class easy to use for new features. The cautionary point is not
to let this future planning delay the development for
the current system. At some point you have to actually deliver
the system to the client.
The design phase should produce a set of classes that can be
used to solve the problem posed by the requirements.
Who is this man and can he help?

(1887 - 1985)
Hint: You can't ask him to solve it.
-
Understand the Problem
- What are you asked to show?
- Can you restate the problem in your own words?
- Can you think of a picture or diagram that might help
you understand the problem?
-
Devise a Plan
- Look for a pattern?
- Draw a picture
- Solve a simpler problem
- Consider special cases
- Have you seen the problem before or the same problem
in a slightly different form?
- Do you know how to solve a related problem? Could that
help solve this problem?
- Be ingenious!
-
Carry out the Plan
This is should be easy if the first two steps are
done well.
Fine! But how does this help find appropriate structures to
use if you haven't already solved the same or a similar problem
or don't feel particularly ingenious at the moment?
You can try the following technique for discovering the set
of classes you may need.
The technique is called CRC and consists of an
approach to discovering classes and member functions to solve a
problem you should already understand.
Classes describe objects.
Function members describe actions or queries about
properties of objects: Change your size! What is your size?
So a simple guide is classes will likely correspond to names of objects (nouns) in a
problem description.
Function members of classes will correspond to actions or
queries (verbs) in the problem description.
Of course, not all nouns will correspond to classes. Data
members of classes will likely correspond to named properties of objects.
The technique for finding both classes and member functions
tries to identify one class at a time
- Class
- Responsibilities of the class - what actions or queries
should this class handle
- Collaborators - what other classes might it need to use to
carry out its responsibilities
From a problem description (or the problem described
again in your own words), make a list of candidate classes from
the named objects (the nouns) in the description.
Decide which nouns are likely to be classes and which other
nouns might be properties (data members) of classes.
Select one of the candidate classes and make a list of what
responsibilities it might have.
For each responsibility think about whether it would
need to use another class to accomplish that responsibility and
if so indicate the other class as a collaborator of this
class.
If you find a collaborator, make it be a separate candidate
class and list its responsibilities
A class may or may not have collaborators.
Clearly this is an iterative process.
If in the CRC process a class has a collaborator, it
uses or depends on the collaborator class.
In this case identify what kind of relationship this is. That
is, is it aggregation or simply use as a local variable or
parameter of a member function.
The output of this approach to the design should be a set of
documented classes that includes the member functions and for
each member function a comment describing what it should do.
The design output does not need to describe data members or
the implementation of the function members of the classes.
UML diagrams could be provided or source code with
comments for each member function.
Relationships between classes could be specified by simple
UML diagrams.
The problem is to provide a program that can print an Invoice
such as the one below. Each invoice will contain the address of
the customer. Each Line Item will print the product description,
the quantity ordered, the price/item, and the total charge for
that quantity of items. The total amount due for all line items
on the invoice should also be printed.
Invoice
EZ Rental
8384 Dempsey
Brooklyn, NY 01234
Description Qty Price Total
------------------------------------------------------
Power Cleaner 1 $49.95 49.95
PX Cleaner Fluid 8 $ 9.00 72.00
M803 Vac Bags 10 $ 2.08 20.80
Amount Due: $142.75
The nouns:
- Invoice
- Address
- LineItem
- Product
- Description
- Quantity
- Price
- Total
- AmountDue
Description and Price are properties (data
members) of the Product class.
Total and AmountDue are computed values rather
than classes or data members.
This leads to candidate classes:
- Invoice
- Address
- LineItem
- Product
Each of these represents a useful concept for the
problem. Make each one a class.
The problem is to print an Invoice. Printing with cout
means Invoice would use the ostream class. However, for
more flexibility about where the printing takes place, make each
class(es) responsible for providing a formatted string that
could be printed to the terminal, to a file, etc.
| Invoice |
| responsibility |
collaborator(s) |
| format the invoice |
|
For handling each responsibility, will this class need to use another
class?
The invoice format includes an address. So the Invoice class
may need to use the Address class.
| Invoice |
| responsibility |
collaborator(s) |
| format the invoice |
Address |
Similarly, the format invoice responsibility will need
to use the LineItem class - the Total for a Line Item will be
the responsibility of the LineItem class. The Amount Due will be
computed.
| Invoice |
| responsibility |
collaborator(s) |
| format the invoice |
Address |
| |
LineItem |
The LineItem class needs to format the item.
To format each item the total for that line item is
needed. So the LineItem has that responsibility, too.
| LineItem |
| responsibility |
collaborator(s) |
| format the line item |
? |
| get line total |
|
How does the LineItem class get the line total?
It depends on the properties of a LineItem object. Quantity
is not a property of a Product; it is a property of a LineItem.
That is, a LineItem consists of a Product and a Quantity.
What about price? The Price is a property of a
Product.
So the LineItem will need to use the Product class to get the
Product price:
| LineItem |
| responsibility |
collaborator(s) |
| format the line item |
Product |
| get line total |
|
The get line total will need the quantity (as a parameter).
| Invoice |
| responsibility |
collaborator(s) |
| format the invoice |
Address |
| |
LineItem |
| |
Product |
These classes must do all the work for formatting
themselves. No collaborators:
The Product class responsibilities:
| Product |
| responsibility |
collaborator(s) |
| format Product |
|
The Address class responsibilities:
| Address |
| responsibility |
collaborator(s) |
| format Address |
|
Each collaboration represents a class relationship.
But is each relationship class member (aggregation) or
as a parameter or local variable (dependent use)?
| Invoice |
| responsibility |
collaborator(s) |
| format the invoice |
Address (class member) |
| |
LineItem (Not a class member; but an array of these as member) |
| |
Product (Not a class member; each LineItem has a Product) |
So the Invoice class
- has an Address member
- has an array member of LineItem's
| LineItem |
| responsibility |
collaborator(s) |
| format the line item |
Product (class member) |
| get line total |
|
So the LineItem class
The result of this CRC design approach should be a set of
classes with documented member functions.
For example, the initial Invoice.h and LineItem.h files will
show the member functions that correspond to the
responsibilities in the CRC design with comments!
class Invoice
{
public:
/**
* Initialize an Invoice for up to maxItems LineItems
*/
Invoice();
Invoice(int maxItems);
Invoice(const Address& addr, int maxItems);
/**
* Formats the invoice
*/
string format();
/**
* Adds a line item for a product and quantity to
* this invoice.
*/
void add(const Product& p, int quantity);
};
class LineItem
{
public:
/**
* Initialize LineItem
*/
LineItem();
LineItem(const Product& p, int quantity);
/**
* format the line item
*/
string format();
/**
* get line item total amount
*/
double getLineTotal();
};
To implement the design:
- Each design class must add data members.
- Each constructor and member function must be implemented.
In addition, we must provide tests for the class(es).
Here is a sample invoice test program:
int main()
{
Address ezAddr("EZ Rental",
"8384 Dempsey",
"Brooklyn",
"NY",
"01234");
Invoice ezInv(ezAddr, 10);
ezInv.add(Product("Power Cleaner", 49.95), 1);
ezInv.add(Product("PX Cleaner Fluid", 9.00), 8);
ezInv.add(Product("M803 Vac Bags", 2.08), 10);
cout << ezInv.format() << endl;
return 0;
}
The responsibilities for formatting and printing the invoice
are distributed over the classes as a result of the design
phase. No one class has to do everything and so each class can
be easier to understand and implement.
In general, this CRC technique can provide an effective way of
modularizing a program using object oriented
features. Classes are designed and Responsibilities are
assigned to the chosen class objects
with the goal of building a system according to specified
requirements.
This test program should produce the invoice:
Invoice
EZ Rental
8384 Dempsey
Brooklyn, NY 01234
Description Qty Price Total
------------------------------------------------------
Power Cleaner 1 $49.95 49.95
PX Cleaner Fluid 8 $ 9.00 72.00
M803 Vac Bags 10 $ 2.08 20.80
Amount Due: $142.75
How can you print the headings:
Description Qty Price Total
so that
- "Description" is left justified in a field of 25 character
positions
- "Qty", "Price" and "Total" are each one right
justifed in a field of 10 character positions.
- include <iomanip>
- Use cout << setw(25) to cause the next item
output be in a field of 25 character positions
- cout.setf(ios::left, ios::adjustfield) causes an
item that needs fewer character positions than the field to be
placed in the left most positions (and padded with the fill
character, i.e., blanks)
- cout.setf(ios::right, ios::adjustfield) causes an
item that needs fewer character positions than the field to be
placed in the right most positions (and padded with the fill
character, i.e., blanks)
cout.setf(ios::left, ios::adjustfield);
cout << setw(15) << "Description";
cout.setf(ios::right, ios::adjustfield);
cout << setw(10) << "Qty"
<< setw(10) << "Price"
<< setw(10) << "Total" << endl;
How can you create a format string instead of printing the
previous heading?
#include <sstream>
string format()
{
stringstream ss;
ss.setf(ios::left, ios::adjustfield);
ss << setw(15) << "Description";
ss.setf(ios::right, ios::adjustfield);
ss << setw(10) << "Qty"
<< setw(10) << "Price"
<< setw(10) << "Total" << endl;
return ss.str();
}
How can you print a line of 55 "-" without typing all 55 characters?
When an output value needs fewer character positions than the output
field, the value is padding with the 'fill' character either on
the left or on the right depending on the justification
desired.
The fill character, by default, is a blank.
But you can change the fill character. For example, you can
make the fill character be '-'.
Then to print 55 '-' characters:
- change the fill character to '-'
- print an empty string in a field of width 55
- change the fill character back to a blank
cout.fill('-');
cout << setw(55) << "" << endl;
cout.fill(' ');
How do you print floats and doubles in fixed decimal format
(not scientific notation) and with 2 decimal places?
- set the output default for floats and doubles to be
fixed
- set the output default precision to be 2 decimal places
cout.setf(ios::fixed);
cout.precision(2);