|

Design? Hmmm -
that's an interesting topic. Programming, at its core, is problem
solving. Due to the nature of the
situation, when you design a
program, your critical thinking and planning skills will be much more
important than your
coding skills. You will always be doing
something you haven't done before. You will always be tackling a
fresh and
complex problem.
| "Just
remember: you're not a "dummy," no matter what those
computer books claim. The real dummies are the
people who, though technically expert,
couldn't design hardware and software that's usable
by normal consumers if their lives depended upon it!"
- Walter Mossberg |
It becomes a logical conclusion,
after observing the current market place, that technology is not what
sells software, but rather, psychology. Many times there are
applications available on the market that are far more powerful and
versatile than those of their competitors. However, these
applications do not succeed because their interface is not as intuitive.
It boils down to the size of the learning curve and the amount of effort
it will take the user to become accustomed to a new program. You
can never, ever underestimate the importance of the user interface.
Menus, text, boxes, graphics,
multimedia, and whatever other components make up the user interface
must be arranged in a fashion that is intuitive and responsive.
The program should be able to handle a good deal of abuse, as users will
never do exactly what a program asks them to do. An application
should be forgiving of errors and as user-friendly as possible. An
intuitive interface will be just as important as any bells and whistles
the application may offer.
Which language will you choose to build the project in, and why?
Does the project need the speed and efficiency of C++? Does it
need the platform independence of Java? Does it need the speed of
development that can be achieved with Visual Basic? As far as
planning goes, you might choose to use a flowchart, a graph, a table or
pseudo code. You might use all of these tools. You might
plan with Word, Visio, or simply sketch out your thoughts on paper.
There is no "right" way to plan. Use what works for you - we are
all different and therefore we all respond to different methods.
Some planning methods:
- Interview -
Start with an interview. The best solution is to take
a tape recorder and record your conversation. If you
can't do this, then take detailed notes when meeting with
the client. The most common mistakes result from
miscommunication. Before you start to pseudocode,
flowchart or code anything, you must be ABSOLUTELY sure of
what your client wants and how they want it. Get it in
writing, and if this stage will involve a lot of time on
your part, be sure you reflect that in your estimate of the
project. If you are creating the project for yourself
- interview yourself! No, I don't mean have an
out-of-body experience. :(
You know what I mean here.
:)
- Flowcharting -
Now that you have some basic information about what kind of
program is required, and what information it will have to
provide, you need to begin to organize your thoughts.
Look at the IPO (Input Output Processing). Plan your
program's behavior, step by step, charting decision
structures, repetition, input, output and processing.
Ask yourself, if the user does this, then what? If my
program receives this type of input, then what?
Visio is an excellent tool for planning. Word,
WordPerfect and Star Office also have good tools for the
planning and flow-charting stage.
- Pseudocode -
Pseudo coding should not be language specific, though many
times people use a C++ or C-ish style when planning.
The idea behind pseudocode is to simply sketch the processes
and behaviors that will need to take place, irrespective of
the language chosen. You focus on logic, behavior and
flow, rather than coding.
- Coding - Some
argue that coding a project should take the least amount of
time (40% - 20% of total project time). While planning
will most probably consume the greatest amount of time,
coding can present its own share of challenges and problems.
This maxim will be violated many, many times as unexpected
changes and modifications arise. The one thing to
remember is that, the more thoroughly you plan the
project, the smoother the coding process will be.
You will never be the all-knowing, omniscient computer oracle that you
aspire to be. A dose of humility can save you a lot of
embarrassment. People who project computer guru personas are
ultimately false, and often hide their insecurities by criticizing the
ignorance of others. You will never have all of the answers, and
there is no reason why you should feel like you have to. The trick
of the trade is, you simply need to learn where you can find the
answers. You will need to place at your disposal many sources of
information:
- A Book Library
- Nothing beats a good library of books on programming
theory and programming languages.
- Class notes -
Keep your notes and handouts from previous computer classes
and courses.
- A portfolio -
Keep a portfolio of all the projects you have worked on.
Keep a portfolio on CD-R, a laptop, or your desktop of all
the projects you work on. Chances are, you will be
able to use some of the code and structures from your past
projects in your new ones.
- A scrapbook -
Keep a scrapbook of favorite routines and algorithms - this
can be very helpful.
- Internet Access
- probably one of your most powerful tools. If you
can't figure out how to code something, chances are you can
find out by researching the topic over the internet.
There are thousands of programming reference sites for you
to bookmark.
- Email Contacts
- Keep contact information on your fellow students, fellow
employees and instructors. Sometimes an acquaintance
is just the resource you need to help you tackle a problem
in a different way, or point out a completely alternative
solution.
We will not go into all the gory details of systems analysis in this
section (aren't you grateful?), but there must be a method to the madness. Remember, one
of the most difficult aspects of beginning a programming project is the
planning stage. What is the nature of the program?
|
Database |
Game |
- 1st
- Gather information. Find out exactly
what kind of information your database will need
to store and how it will be required to present
it.
- 2nd
- Plan your classes, structures and
relationships with tables, charts, and
pseudocode. Practice normalization (1 NF,
2NF, and 3NF).
- 3rd
- Decide on a language. Visual Basic, C++.
SQL queries, Access, Java?
- 4th
- Design the user interface, the back end, and
the intermediate structures.
|
- 1st
- What will the plot of your game be? What
motivation does the player have?
- 2nd
- Create the interface. Paint the scene,
instantiate the environment where the action
will take place. Whether it is console
based or it uses the GUI, once you define the
environment, you may then code how the player
will interact with it. This involves
populating the game with objects.
- 3rd
- Create the flow. Plan each possible
decision of the player. Allow the options
of what the player can do to change based on
each decision she/he makes. You must code
your game so that the player can interact with
the objects and the objects with the player.
The player itself is an object, so you might
include accessor methods for object interaction.
|
Questions you might ask yourself or a client: 1. Do I really need customized software?
2. Is there some other application that already does this?
3. Can I modify a third-party application, or do I need to produce an
application from scratch? 4. Do I truly understand the nature of the problem and EXACTLY what the
client wants?
Simulation
Technique - A simulation is a computer model of a real world system.
A good design using the simulation technique starts with what questions you hope the simulation will answer.
There are four phases:
1. Conceptualization phase -
Ask what the customer hopes to gain from the program. What is it for? What questions must it answer? Think about what is inside and outside of the program.
2. Analysis phase - Help the customer understand what he requires from the program. What behavior will
the program exhibit? What kinds of interactions do the customers expect?
3. Use case - A series of documents, a description of how
the system will be used. It describes interactions and use patterns, helping
the programmer capture goals and requirements for the system.
4. High level design - Don't be concerned about
the language, platform or operating system. Focus on how the system will work. What are the major components? How do they interact with
each other? Set aside issues relating to user interface and focus on components of the problem space.
Think of the responsibilities of objects, what information they hold, how they
will collaborate and interact with other objects. You
may set up a problem space - a set of problems and
issues the program is trying to solve. You may also
set up a solution space - a set of possible solutions to those problems.
Example #1: Design an alarm simulation:
The Scenario: Ormond Beach City Hall has hired you as a
consultant and programmer to create custom software to combine
their fire and theft detection systems. You do a lot of
research on alarm systems. You interview employees and key
people thoroughly. You find out EXACTLY what their current
two systems do, and you ask the right questions to determine
what it is they want their new, combined system to do.
The Sensor Object: After the initial interview
and some research, you decide that the basic object of this
program will be a sensor. There are many derivatives of a
sensor - trip, window, door, smoke, and sound sensors. You
therefore decide to make "Sensor" your base class, and all other
"Sensor" objects will derive from this base class. The
base class, "Sensor" represents many different kinds of sensors,
so "Sensor" would make a good ADT. As such, it would provide a complete interface for all types of
Sensors, and each derived type would provide the implementation.
Clients could use each type of Sensor without regard to which type they were or how they worked. The ADT should handle all the needs of each derived class. You will need a complete understanding of what sensors do,
that is their interactive behavior, as opposed to how they work.
The Other objects: The alarm, the program,
keeping a log, a timer object, etc. These will all interact
with "Sensor" objects, based upon the information you have
gleaned. What are the classes? If you have a class "HeatSensor", it will have to derive from "Sensor". If it makes periodic reports, it may also derive
via multiple inheritance (polymorphically) from the Timer class, or it may have
a Timer as a data member. Practice both encapsulation and
data hiding. Now that we have some of the details
planned out, we turn to design goals>
Incorporating OOP Philosophy into the Design:
Encapsulation -
Each class should have a coherent and complete set of responsibilities that no other class has.
"DO one contained thing, and do that thing well." Example: While a Sensor class may note and log the current temperature, it may delegate the
responsibility of recording that data to a Log object. Maintain a FIRM division of responsibilities. Changes to one class should not affect others.
Virtual methods - Should HeatSensor have a ReportAlarm() method? All sensors will need the ability to report an alarm. This is a good indication that ReportAlarm() should be a virtual method of
the base class Sensor, and that Sensor may be an abstract base class. The overridden ReportAlarm()
methods from each derived class will fill in the details
they are uniquely qualified to supply.
Event loops - Typically, infinite while(true) loops will
be used that get messages from the OS (mouse clicks, keyboard) and dispatch them
one by one, returning to the loop until an exit condition
(sentinel value) is satisfied. Example:
#include <iostream.h>
//-------------------------------------------------------------------------------------
class Condition
{
public:
Condition() { }
virtual ~Condition() {}
virtual void Log() = 0;
};
//-------------------------------------------------------------------------------------
class Normal :
public Condition
{
public:
Normal() { Log(); }
virtual ~Normal() {}
virtual void Log() { cout << "Logging normal conditions...\n"; }
};
//-------------------------------------------------------------------------------------
class Error :
public Condition
{
public:
Error() {Log();}
virtual ~Error() {}
virtual void Log() { cout << "Logging error!\n"; }
};
//-------------------------------------------------------------------------------------
class Alarm :
public Condition
{
public:
Alarm ();
virtual ~Alarm() {}
virtual void Warn() { cout << "Warning!\n"; }
virtual void Log() { cout << "General Alarm log\n"; }
virtual void Call() =
0;
};
Alarm::Alarm()
{
Log();
Warn();
}
//-------------------------------------------------------------------------------------
class FireAlarm :
public Alarm
{
public:
FireAlarm(){Log();};
virtual ~FireAlarm() {}
virtual void Call() { cout<< "Calling Fire Dept.!\n"; }
virtual void Log() { cout << "Logging fire call.\n"; }
};
//-------------------------------------------------------------------------------------
int main()
{
int input;
int okay = 1;
Condition * pCondition;
while(okay)
{
cout << "(0)Quit (1)Normal (2)Fire: ";
cin >> input;
okay = input;
switch(input)
{
case 0: break;
case 1:
pCondition = new Normal;
delete pCondition;
break;
case 2:
pCondition = new FireAlarm;
delete pCondition;
break;
default:
pCondition = new Error;
delete pCondition;
okay = 0;
break;
} //close
switch
} //close while
true
return 0;
} |
Output:
(0)Quit (1)Normal (2)Fire: 1
Logging normal conditions...
(0)Quit (1)Normal (2)Fire: 2
General Alarm log
Warning!
Logging fire call.
(0)Quit (1)Normal (2)Fire: 0
Calling virtual member functions from a constructor can cause confusing results if you are not mindful of the order of construction of objects. When the FireAlarm object is created,
by inheritance the order of construction is: Condition, Alarm, FireAlarm. The Alarm constructor calls Log(), but it is Alarm's Log() function that is invoked rather than FireAlarm's Log() function, despite Log() being declared virtual. This is because at the time Alarm's constructor runs there is no FireAlarm object. Later, when FireAlarm is finally constructed, its constructor calls Log() again and this time FireAlarm::Log() is called
instead of the base class's Log().
Example #2:
Design an Email Manager
You set out to design a Program to manage e-mail from multiple clients.
Remember the old adage, measure twice, but cut once. The
prevailing philosophy here is, divide and conquer. After gathering
extensive information and through multiple interviews, you decide to
divide this into sub-projects. So far, you have decided to
split the program into these specific modules (you will link them
together later):
1 - Communications
2 - Database
3 - E-mail
4 - Editing
5 - Platform issues
6 - Extensibility
7 - Organization and Scheduling
We further divide the implementation into:
1 - Communication
2 - Message Format
3 - Message Editors
Focus: Focus on the message format first. There is no point in presenting information to
the user until you know what information you are dealing with. We will convert each e-mail from its original format, to
the MegaMail 1.0 format, so that only one record format will have to be handled and reading and writing to the disk will be simplified.
Process: Avoid being concerned with implementation. Focus on designing a clean interface between classes and delineating what data and methods each class will need. Design initial classes,
stubbing functions out where you need to. Design around base classes, ADT's and derived classes. Nouns that relate to the subject, in reality, make good candidates for object names.
Their attribute (the has-a relationship), will become the data members. Verbs that relate to the subject, in reality, make good function and method names.
Inheritance: Through inheritance, each derived class should
acquire all the data and functions of its base class while having one discrete, additional capability. You may wish to purchase libraries, rather than creating your own, to save
yourself a lot of time. Don't forget to use the STL when you can.
Prototype: First, code a working, stripped down prototype. Use the 80/80 rule.
You can't please all of the people all of the time, but you can try to code for what 80% of the people want 80% of the time. Use API's if applicable.
The most frequent cause of software dying before its release is that there was not sufficient agreement about what was being built.
Before you start anything, GET THE INFORMATION YOU NEED, get it in
writing, get it nailed down into something concrete. Make sure you
are in agreement with and that you understand the client.
API - Application Programming Interface, a set of routines and documentation for using a service. Most mail providers will give you an API.
In this way, you can code your program to work with other programs
installed in the operating system.
Inheritance Hierarchies:
- Rooted - Share
a common base class, all classes descend from a common root
class. The advantage is that multiple inheritance is often avoided.
The disadvantage is that many times implementation will percolate up into the base class.
- Non-rooted -
Objects do not all share a common base class. There is
more than one inheritance hierarchy.
The interface for MegaMail 1.0, so far,
is:
class MegaMailMessage :
public MailMessage
{
public:
MegaMailMessage();
MegaMailMessage(pAddress Sender,
pAddress Recipient,
pString Subject,
pDate creationDate);
//Other constructors
will go here. Also copy constructor.
~MegaMailMessage();
//Accessor methods
pAddress & GetSender();
void SetSender(pAddress &);
//Overloaded
operator methods here, including operator equals and conversion routines to
//turn MegaMail messages into messages of other formats.
private:
pAddress itsSender;
pAddress itsRecipient;
pString itsSubject;
pDate itsCreationDate;
pDate itsLastModDate;
pDate itsReceiptDate;
pDate itsFirstReadDate;
pDate itsLastReadDate;
}; |
Driver program - a driver
program is a function that exists only to test or demonstrate other functions. Example:
#include <iostream.h>
#include <string.h>
typedef unsigned long pDate;
enum SERVICE { MegaMail, Interchange, CompuServe, Prodigy, AOL, Internet };
//-------------------------------------------------------------------------------------
class String
{
public:
//Constructors
String();
String(const char * const);
String(const String &);
~String();
//Overloaded operators
char &
operator[](int offset);
char operator[](int offset)
const;
String operator+(const String &);
void
operator+=(const String&);
String & operator= (const String &);
//Overloaded operator << needs to be a friend class
so objects outside the class can access it.
friend ostream &
operator<<(ostream & theStream, String & theString);
//Accessors
int GetLen()const {
return itsLen; }
const char * GetString()
const {
return itsString; }
//Static member data, must be defined outside the
class
static int ConstructorCount;
private:
String (int);
//private constructor
char * itsString;
int itsLen;
};
//Initializing static member data
int
String::ConstructorCount = 0;
//Overloaded
default constructor, creates string of 0 bytes
String::String()
{
itsString = new char[1];
itsString[0] = '\0';
itsLen=0;
cout << "\tDefault string constructor\n";
ConstructorCount++;
}
//Overloaded (helper) constructor, used only by class methods for creating a new string of required size. Null filled.
String::String(int len)
{
itsString = new char[len+1];
int i;
for( i = 0; i<=len; i++)
itsString[1] = '\0';
itsLen=len;
cout << "\tString(int) constructor\n";
ConstructorCount++;
}
//Overloaded constructor, converts a character array to a String
String::String(const char *
const cString)
{
itsLen = strlen(cString);
itsString = new char[itsLen+1];
int i;
for( i = 0; i<itsLen; i++)
itsString[i] = cString[i];
itsString[itsLen]='\0';
cout << "\tString(char*) constructor\n";
ConstructorCount++;
}
//Copy constructor
String::String (const String & rhs)
{
itsLen=rhs.GetLen();
itsString = new char[itsLen+1];
int i;
for(i = 0; i<itsLen;i++)
itsString[i] = rhs[i];
itsString[itsLen] = '\0';
cout << "\tString(String&) constructor\n";
ConstructorCount++;
}
//Destructor, frees allocated memory
String::~String ()
{
delete [] itsString;
itsLen = 0;
cout << "\tString destructor\n";
}
//Overloaded operator =
String & String::operator=(const String & rhs)
{
if(this == &rhs)
return *this;
delete [] itsString;
itsLen=rhs.GetLen();
itsString = new char[itsLen+1];
int i;
for(i = 0; i<itsLen;i++)
itsString[i] = rhs[i];
itsString[itsLen] = '\0';
return
*this;
cout << "\tString operator=\n";
}
//Overloaded non-constant offset operator, returns
a reference to character so it can be changed
char & String::operator[](int
offset)
{
if(offset > itsLen)
return itsString[itsLen-1];
else
return itsString[offset];
}
//Overloaded constant offset operator for use on const objects (see copy constructor)
char String::operator[](int offset)
const
{
if(offset > itsLen)
return itsString[itsLen-1];
else
return itsString[offset];
}
//Overloaded
operator +, creates a new string by adding a current string to rhs,
concatenates
String String::operator+(const String & rhs)
{
int totalLen = itsLen + rhs.GetLen();
String temp(totalLen);
int i,j;
for(i = 0; i<itsLen; i++)
temp[i] = itsString[i];
for(j = 0; j<rhs.GetLen(); j++, i++)
temp[i] = rhs[j];
temp[totalLen]='\0';
return temp;
}
//Overloaded
operator +=, changes the current string, returns nothing
void String::operator+=(const String& rhs)
{
int rhsLen = rhs.GetLen();
int totalLen = itsLen + rhsLen;
String temp(totalLen);
int i,j;
for(i = 0; i<itsLen; i++)
temp[i] = itsString[i];
for(j = 0; j<rhs.GetLen(); j++, i++)
temp[i] = rhs[i-itsLen];
temp[totalLen]='\0';
*this = temp;
}
//Definition of our friend class, overloading the
ostream operator <<. MUST be defined outside
class.
ostream & operator<<( ostream & theStream, String & theString)
{
theStream << theString.GetString();
return theStream;
}
//-------------------------------------------------------------------------------------
class pAddress
{
public:
pAddress(SERVICE theService,
const String & theAddress,
const String & theDisplay):
itsService(theService),
itsAddressString(theAddress),
itsDisplayString(theDisplay)
{}
//pAddress(String, String);
//pAddress();
//pAddress (const pAddress&);
~pAddress(){}
//Friend function overloads the ostream << ooperator
friend ostream & operator<<( ostream & theStream, pAddress & theAddress);
//Accessor
method
String & GetDisplayString() { return itsDisplayString; }
private:
SERVICE itsService;
String itsAddressString;
String itsDisplayString;
};
ostream & operator<<( ostream & theStream, pAddress & theAddress)
{
theStream << theAddress.GetDisplayString();
return theStream;
}
//-------------------------------------------------------------------------------------
class MegaMailMessage
{
public:
//MegaMailMessage();
MegaMailMessage(const pAddress & Sender,
const pAddress & Recipient,
const String & Subject,
const pDate & creationDate);
~MegaMailMessage(){}
void Edit();
//invokes editor on this message
pAddress & GetSender() { return itsSender; }
pAddress & GetRecipient() { return itsRecipient; }
String & GetSubject() { return itsSubject; }
//void SetSender(pAddress& );
//other member accessors
//Overloaded operator methods here, including operator equals and conversion routines
//to turn MegaMail messages into messages of other formats.
private:
pAddress itsSender;
pAddress itsRecipient;
String itsSubject;
pDate itsCreationDate;
pDate itsLastModDate;
pDate itsReceiptDate;
pDate itsFirstReadDate;
pDate itsLastReadDate;
};
//MegaMail
constructor
MegaMailMessage::MegaMailMessage(
const pAddress & Sender,
const pAddress & Recipient,
const String & Subject,
const pDate & creationDate):
itsSender(Sender),
itsRecipient(Recipient),
itsSubject(Subject),
itsCreationDate(creationDate),
itsLastModDate(creationDate),
itsFirstReadDate(0),
itsLastReadDate(0)
{
cout << "MegaMail Message created. \n";
}
void MegaMailMessage::Edit()
{
cout << "MegaMailMessage edit function called\n";
}
//-------------------------------------------------------------------------------------
int main()
{
pAddress Sender(MegaMail, "cgermany@networkingprogramming.com", "Charles
Germany");
pAddress Recipient(MegaMail, "jdoe@aol.com","Jane Doe");
MegaMailMessage MegaMailMessage(Sender, Recipient, "Can
we meet sometime today?", 0);
cout << "Message review... \n";
cout << "From:\t\t" << MegaMailMessage.GetSender() << endl;
cout << "To:\t\t" << MegaMailMessage.GetRecipient() << endl;
cout << "Subject:\t" << MegaMailMessage.GetSubject() << endl;
return 0;
} |
Output:
MegaMail Message created.
Message review...
From: Charles Germany
To: Jane Doe
Subject: Can we meet sometime today?
As we start out, pDate is typedefined to be an unsigned long.
This is just a placeholder, eventually pDate may become an entire class
to deal with Date objects. Nest we add the enumerated constant, SERVICE.
This allows Address objects to keep track of what type they are. Then,
once again, we rewrite the interface to and implementation of String.
The String class is used for member variables in all message classes and
in classes used by messages (not the same as string.h). Next, the pAddress class
is declared. It will later be expanded for forwards, replies, etc.
Service gets tracked as an enumerated const that is a member variable of each pAddress object.
Then we add the interface to MegaMailMessage class. Later it may be made part of
our inheritance hierarchy. The Edit() function is stubbed out to indicate where it will be when
the class is fully operational. The edit function may be several
classes and call many functions in and of itself, since it will require
the features of a full-blown word processor. The main() function
is the driver program. It Accesses accessor functions and the
overloaded operator<< .
Summary:
In accordance with data hiding, clients of the object should not need to understand the implementation details of how they fulfill their responsibilities. Procedural languages such as C and
Pascal are collections of procedures, whereas object-oriented languages
such as C++ and Java are collections of classes. Remember that key
and fundamental difference. Object-oriented programming focuses on integrated data and functions as discrete units that have BOTH data and functions. Procedural programming focuses on functions and how they act on data. C++ provides programmers with
the tools necessary to manage projects of great complexity. That is one
of its many advantages over procedural languages.
©2004 C. Germany
|