Section 7 - Analysis of an OOP Project
7A.7.1 General Strategy: Class vs. Test Main
When we are given a project description in this course, there are two aspects to consider. The first is the class specification. The class is intended for many different users, not just you. So you must define it independently of the actual client application (i.e., independently of main()) and only include data and methods useful to the wide range of potential applications. The second is the client, which usually means the main(). The client is really only a test of the class, but I do give you requirements for its behavior, as well, so you must design the main() carefully and according to spec. Think of these as two distinct parts of the task.
Our approach will be to first build the class and test it in a small client (main()). In fact, we won't even try to build the entire class at once. We will do it in stages, completing and testing each stage before we move on to the next.
7A.7.2 The Example Project Specification
You are to define a class called Patient.
Patient will have the following instance members:
- name, a string.
- id, an int.
- temperature, a double.
The id is meant to represent a patient ID that might be used to track a patient in an E.R. or hospital. The temperature is the Fahrenheit temperature that the patient presents upon admission.
The class should only allow string names that have between 2 and 40 characters. It will only allow ids that are between 0 and 9999. It will only allow temperatures between 88 and 111 degrees. Any other value will be considered a user error and we will not allow the client to set such values. This is true of all applications that use the class, so these limits are built-into the class, not the client. These limits, which are part of the class, should be public static members that are constants. Also, there should be an alarm temperature of 103.5 (static) that will be used to set off an alarm. I will describe the alarm in a moment.
You should provide the following instance methods:
- Default and parameter-taking constructors.
- A mutator and accessor for each instance member.
- A display() method that clearly shows the patient's data. If the patient's temperature is above the alarm temperature, we will add a line "*** urgent: attend immediately ***".
Provide a test main that instantiates two Patient objects and gets the data for each of them from the user, interactively. It then compares the temperatures of the two Patient objects and displays the names of the patients, making sure that the name of the patient with the higher temperature is listed first (under the assumption that a patient with a higher temperature requires more immediate attention than a patient with a lower one). Test this in multiple runs and show that the patient with the higher temperature is always displayed first.
7A.7.3 Constructor and display() - The Framework Phase
The first step is to get the framework of the class and a small main() designed and debugged. We will do the minimum necessary to create a working program, then build from there. We will call this the Framework Phase. Often, if you are stuck deep in the implementation of an assignment, I will ask to see your Framework Phase so save a copy of it in a separate file before you move on to the next phases.
In the Framework Phase we create only:
- The default constructor.
- The display() method.
- A test main() that instantiates two objects and displays them, without any user input.
We are not doing any mutators. We are not doing anything with static members. One step at a time, thank you very much.
You should be able to do this small part in about 15 minutes. Try it, and after you get it working, look at my solution:
// ---------------- SOURCE ------------------------- #include <iostream> #include <string> using namespace std; // class prototype class Patient { private: string name; int id; double temperature; public: // Default constructor Patient(); void display(); }; int main() { // instantiate two Patients Patient person1, person2; // display both person1.display(); person2.display(); return 0; } // class method definitions Patient::Patient() { name = "nobody"; temperature = 98.6; id = 0; } void Patient::display() { cout << "Patient: " << "\n Name: " << name << "\n ID: " << id << "\n Body Temperature: " << temperature << " (F)" << endl; } /* ------------- RUN (OUTPUT) ---------------------- Patient: Name: nobody ID: 0 Body Temperature: 98.6 (F) Patient: Name: nobody ID: 0 Body Temperature: 98.6 (F) Press any key to continue . . . --------------- END OF RUN ------------------------- */
This was easy, but you have to actually do it to reap the benefit. Also, you can't just write it. You have to fully debug and run it.
Now we have a framework on which we can bang, cut, drill and craft.
7A.7.4 Adding the Statics and Mutators - The Class Phase(s)
We are going to add the rest of the class members but not do the full main() yet. We will still keep main() very simple, to test out the class and debug it if necessary.
We add the static members:
- The limits for the string length. The spec did not say what to call them, so we can pick our own names. If it had specified the names of these statics, we would be bound to name them as described. We'll call them MIN_LENGTH and MAX_LENGTH.
- The limits for the id. Same thing: we'll name this MIN_ID and MAX_ID.
- The limits for the temperature. You guessed it, MIN_TEMP and MAX_TEMP.
- The alarm value, which we will call ALARM_TEMP.
We also add:
- Accessors for each member
- Mutators for each member
- The parameter-taking constructor
We will write a test main that does the following:
- Instantiates three Patient objects, one using the parameter-taking constructor.
- Manually sets patient #1 to "Fred", 1234 and 100.3 and patient #2 to "Janis", 5555, 103.7.
- Displays all patients.
- Calls a mutator with an illegal value and confirm that it returns false.
- Uses a couple accessors to print data from one of the objects to the screen.
This is the Class Phase, and sometimes it is broken into sub-phases. For instance, if there are a lot of class methods, you might do a few mutators, get those working, add a few more, get that working, then add some more elaborate instance methods that might not be mutators or accessors. As with the Framework Phase, I expect that you will have done this if you are asking for help to debug the way the assigned client interacts with the assigned class. You would not be attempting to write the assigned client if you had not first done the Class Phase, with a simple client and debugged that. (Of course, you can always ask for help if you are working on the Class Phase and get stuck.)
In our current effort, if we have done everything correctly, we'll see the alarm on Janis, but not on Fred. Again, please try it yourself. It is better for you to get stuck and ask questions on this in the public forums rather than to wait and be overwhelmed in an assignment.
// ---------------- SOURCE ------------------------- #include <iostream> #include <string> using namespace std; // class prototype class Patient { public: static const int MIN_LENGTH = 2; static const int MAX_LENGTH = 40; static const int MIN_ID = 0; static const int MAX_ID = 9999; static const double MIN_TEMP; // we use alternate initialization, below static const double MAX_TEMP; static const double ALARM_TEMP; static const string DEFAULT_NAME; static const int DEFAULT_ID = 0; static const int DEFAULT_TEMP = 98.6; private: string name; int id; double temperature; public: Patient(); Patient( string name, double temperature, int id ); void display(); // Accessors can be done in line: double getTemperature() { return temperature; } int getID() { return id; } string getName() { return name; } // mutators bool setTemperature(double temperature); bool setID(int id); bool setName(string name); }; // alternate way to initialize a static (required for not const ints) const double Patient::ALARM_TEMP = 103.5; const double Patient::MIN_TEMP = 88.; const double Patient::MAX_TEMP = 111.; const string Patient::DEFAULT_NAME = "nobody"; int main() { // instantiate three Patients Patient person1, person2; Patient person3("Racha", 98.7, 32); // make changes to two person1.setName("Fred"); person1.setID(1234); person1.setTemperature(100.5); person2.setName("Janis"); person2.setID(5555); person2.setTemperature(103.7); // display all person1.display(); person2.display(); person3.display(); // test a couple mutators for data filtering if ( !person1.setTemperature(333) ) cout << "Unable to set temperature to 333." << endl; else cout << "Temp set to 333." << endl; if ( !person2.setID(-44) ) cout << "Unable to set ID to -44." << endl; else cout << "ID set to -44." << endl; cout << endl; // test a few accessors cout << "Patient #1's name is " << person1.getName() << endl; cout << "The minimum valid temperature is " << Patient::MIN_TEMP << endl; return 0; } // class method definitions Patient::Patient() { name = DEFAULT_NAME; temperature = DEFAULT_TEMP; id = DEFAULT_ID; } Patient::Patient( string name, double temperature, int id ) { if ( !setName( name ) ) this->name = DEFAULT_NAME; if ( !setTemperature( temperature ) ) this->temperature = DEFAULT_TEMP; if ( !setID( id ) ) this->id = DEFAULT_ID; } void Patient::display() { cout << "Patient: " << "\n Name: " << name << "\n ID: " << id << "\n Body Temperature: " << temperature << " (F)"; if (temperature > ALARM_TEMP) cout << "\n *** urgent: attend immediately ***"; cout << endl; } // mutators bool Patient::setTemperature(double temperature) { if (temperature < MIN_TEMP || temperature > MAX_TEMP) return false; this->temperature = temperature; return true; } bool Patient::setID(int id) { if (id < MIN_ID || id > MAX_ID) return false; this->id = id; return true; } bool Patient::setName( string name) { if (name.length() < MIN_LENGTH || name.length() > MAX_LENGTH) return false; this->name = name; return true; } /* ------------- RUN (OUTPUT) ---------------------- Patient: Name: Fred ID: 1234 Body Temperature: 100.5 (F) Patient: Name: Janis ID: 5555 Body Temperature: 103.7 (F) *** urgent: attend immediately *** Patient: Name: Racha ID: 32 Body Temperature: 98.7 (F) Unable to set temperature to 333. Unable to set ID to -44. Patient #1's name is Fred The minimum valid temperature is 88 Press any key to continue . . .------------------- */
Notice that I defined the parameter-taking constructor so that it calls the mutators. Once you have mutators, you should use them from that constructor so that all changes to members go through the mutator methods and nowhere else. This is more important in constructors that take parameters, and it is optional in default constructors since you may assign values directly there.
This probably took about an hour or two to write and debug. You could have broken this into steps and done only one mutator and accessor first, say, the mutator and accessor associated with the name member, debugged it, then went on to do the others.
Note that this main() did not completely test everything. Ideally, you would do a thorough check of your class methods before proceeding to the next phase. Keep a copy of the final Class Phase and client so you can show them to me if you need help with something further down stream. I'll ask to see them to help you isolate your problem.
At this point our class is about done and all we need to do is design and write our sample main(). Next page.