Team LiB
Previous Section Next Section

The Student Class (Dynamic Data Retrieval; Persisting Object State)

Aside from making a minor change to the ScheduleSection method of the Course class that we just discussed, and "refurbishing" the ScheduleOfClasses class that we previously discussed, the only other domain class that we wish to enhance among those that we created in Chapter 14 is the Student class. To demonstrate the techniques of dynamic data retrieval and data persistence, we're going to

Much of the Student class's code remains unchanged from the way that it was presented in Chapter 14, so we'll only touch upon the significant changes that we've made to the class here.

Initializing a Student's State

The first significant change that we've made is in the Student class constructor. Because this code is fairly involved, we'll present the constructor here in its entirety first, and will then narrate it.

  //----------------
  // Constructor(s).
  //----------------

  public Student(string ssn) : this() {
    // First, construct a  "dummy" Student object.  Then,
    // attempt to pull this Student's information from the
    // appropriate file (ssn.dat:  e.g., 111-11-1111.dat).
    // The file consists of a  header record, containing
    // the student's basic info. (ssn, name, etc.), and
    // 0 or more subsequent records representing a  list of
    // the sections that he/she is currently registered for.
  string line = null;
  StreamReader srIn = null;

  // Formulate the file name.

  string pathToFile = ssn + ".dat";

  try {
    // Open the file.

    srIn = new StreamReader(new FileStream(pathToFile,FileMode.Open));

    // The first line in the file contains the header
    // information, so we use ParseData() to process it.

    line = srIn.ReadLine();
    if (line != null) {
     ParseData(line);
    }

    // Remaining lines (if there are any) contain
    // section references.  Note that we must
    // instantiate an empty vector so that the
    // ParseData2 method may insert
    // items into the ArrayList.

    attends = new ArrayList();

    line = srIn.ReadLine();

    // If there were no secondary records in the file,
    // this "while" loop won't execute at all.

    while (line != null) {
     ParseData2(line);
     line = srIn.ReadLine();
    }

    srIn.Close();
  }
  catch (FileNotFoundException f) {
    // Since we are encoding a  "dummy" Student to begin
    // with, the fact that his/her name will be equal
    // to "???" flags an error.  We have included
      // a  boolean method SuccessfullyInitialized
      // which allows client code to verify the success
      // or failure of this constructor (see code below).
      // So, we needn't do anything special in this
      // "catch" clause!
    }
    catch (IOException i) {
      // See comments for FileNotFoundException above;
      // we needn't do anything special in this
      // "catch" clause, either!
    }

    // Create a  brand new Transcript.
    // (Ideally, we'd read in an existing Transcript from
    // a  file, but we're not bothering to do so in this
    // example).

    this.Transcript = new Transcript(this);
  }

Before we discuss the preceding constructor in depth, let's take a look at a second parameterless constructor that we've created for the Student class, because as we learned in Chapter 13, if we overload the constructor for a class, then the default parameterless constructor is unavailable unless we explicitly replace it.

All of the attributes are set to the value "???" in this constructor; as we'll see later, if these never get overwritten with the proper values from a Student's file, then we'll be able to detect that an error has occurred.

  // A  second form of constructor, used when a  Student's data
  // file cannot be found for some reason.

  public Student() : base("???", "???") {
    // Reuse the code of the parent's (Person) constructor.
    // Question marks indicate that something went wrong!

    this.Major = "???";
    this.Degree = "???";

    // Placeholders for the remaining attributes (this
    // Student is invalid anyway).

    this.Transcript = new Transcript(this);
    attends = new ArrayList();
  }

Now, let's go back and walk through the first constructor. Rather than simply passing in hard-coded attribute values as constructor arguments, we're going to attempt to read these from the appropriate student data file. Therefore, the constructor now expects only one argument: the Student-to-be-constructed's ssn, which will be used to formulate the data file name (ssn.dat):

public Student(string ssn) : this() {

The first thing this constructor does is to construct a "dummy" Student object using the parameterless Student constructor. (Recall that the this keyword can be used to call a different constructor declared by the same class, a concept that was discussed in Chapter 13.)

Next, we'll attempt to open and read the appropriate data file, using a technique very similar to the technique used in the InitializeObjects method of CollectionWrapper. (If we were to have made Student a derived class of CollectionWrapper, then we could have simply inherited InitializeObjects; but, such inheritance would truly compromise the "is a" relationship—far more severely than we did with the ScheduleOfClasses class [when we knew it didn't need to parse two files, but extended CollectionWrapper anyway]—because a Student is clearly not a collection! So, we resist the urge to "misuse" inheritance, and instead write code that looks remarkably similar to the InitializeObjects method.)

    string line = null;
    StreamReader srIn = null;
    bool outcome = true;

    // Formulate the file name.

    string pathToFile = ssn + ".dat";

    try {
      // Open the file.

      srIn = new StreamReader(new FileStream(pathToFile), FileMode.Open);

Note that we use two different methods once again—ParseData and ParseData2— but back-to-back in this case, because although we're only reading one student data file, there are two different record formats within a single file: the primary record containing student information, and zero or more secondary records containing section numbers for which the student has registered.

    // The first line in the file contains the header
    // information, so we use ParseData to process it.
      line = srIn.ReadLine();
      if (line != null) {
        ParseData(line);
      }

      // Remaining lines (if there are any) contain
      // section references.  Note that we must
      // instantiate an empty vector so that the
      // ParseData2 method may insert
      // items into the ArrayList.

      attends = new ArrayList();

      line = srIn.ReadLine();

      // If there were no secondary records in the file,
      // this "while" loop won't execute at all.

      while (line != null) {
        ParseData2(line);
        line = srIn.readLine();
      }

      srIn.close();
    }

If anything goes wrong while attempting to read the student's data file (e.g., the file isn't found or can't be accessed, or it contains incorrectly formatted data), then one or the other of the catch clauses will be invoked. But, since we've already gone to the trouble of executing the generic constructor (with no arguments) as the first step of this constructor, then we have a "bare bones" (albeit somewhat useless) Student object full of "???" values.

There really isn't any way for a constructor to return an error flag (say, a bool true/false)—by definition, a constructor returns an object (of type Student, in this case)—so we have to devise an alternative way for the client code to detect whether or not the Student object initialization succeeded or failed. We'll do so by creating a separate method called StudentSuccessfullyInitialized in a moment.

    catch (FileNotFoundException f) {
      // Since we are encoding a  "dummy" Student to begin
      // with, the fact that his/her name will be equal
      // to "???" flags an error. We have included
      // a  boolean method StudentSuccessfullyInitialized
      // which allows client code to verify the success
      // or failure of this constructor (see code below).
      // So, we needn't do anything special in this catch
      // clause!
    }
    catch (IOException i) {
      // See comments for FileNotFoundException earlier;
      // we needn't do anything special in this catch
      // clause, either!
    }

    // Create a  brand new Transcript.
    // (Ideally, we'd read in an existing Transcript from
    // a  file, but we're not bothering to do so in this
    // example).

    this.Transcript = new Transcript(this);
  }

StudentSuccessfullyInitialized Method

We were concerned about how to return failure status from a constructor. This turns out to be a rather simple matter: since we've created a "dummy" Student to begin with, the fact that the student's name will be equal to "???" constitutes an error. So, we provide a Boolean method, StudentSuccessfullyInitialized, to allow client code to verify the success or failure of this constructor after the fact.

  // Used after the constructor is called to verify whether or not
  // there were any file access errors.

  public bool StudentSuccessfullyInitialized() {
    if (this.Name.Equals("???")) {
      return false;
    }
    else {
      return true;
    }
  }

Here is an example of how the preceding method would be used in client code (say, in the Main method of the SRS application):


// Excerpt from the Main method of SRS:

string ssn;

// Obtain an ssn value as a user input when he/she logs on (details omitted;
// we'll see how this is done via a GUI in Chapter 16).

// Invoke the new form of Student constructor, which will attempt to read the
// appropriate data file behind the scenes.

Student s = new Student(ssn);

// Now, check to see if the Student's data was successfully retrieved!

if (s.StudentSuccessfullyInitialized()) {
  // Success! Do whatever is appropriate in this case ... details omitted.
}
else {
  // Failure! Do whatever is appropriate in this case ... details omitted.
}
Note?/td>

Another way to indicate that a Student object hadn't been properly initialized would be by utilizing a user-defined exception class. If a problem occurred during Student initialization, the Student constructor could throw an instance of a user-defined exception—for example, a StudentImproperlyInitializedException. The client code calling the Student constructor would then be placed in a try-catch block in order to detect and handle any instances StudentImproperlyInitializedException. The concept of user-defined exceptions was mentioned in Chapter 13.

ParseData and ParseData2 Methods

Because the logic of the ParseData and ParseData2 methods of Student is so similar to the ParseData and ParseData2 methods that we've discussed for these other classes, we won't review them in detail here, but merely present their code. First, here is the ParseData method code listing:

  public void ParseData(string line) {
    // We're going to parse tab-delimited records into
    // four attributes -- ssn, name, major, and degree.
    // We'll use the Split method of the System.String class to split
    // the line we read from the file into substrings using tabs
    // as the delimiter.

    string[] strings = line.Split('\t');

    // Now use the substring values to update the
    // appropriate Student properties.

    this.Ssn = strings[0];
    this.Name = strings[1];
    this.Major = strings[2];
    this.Degree = strings[3];
  }

Here is the ParseData2 method code listing:

  public void ParseData2(string line) {
    // The full section number is a  concatenation of the
    // course no. and section no., separated by a  hyphen;
    // e.g., "ART101 - 1".

    string fullSectionNo = line.Trim();
    Section s = SRS.scheduleOfClasses.FindSection(fullSectionNo);

    // Note that we are using the Section class's enroll()
    // method to ensure that bidirectionality is established
    // between the Student and the Section.
    s.Enroll(this);
  }

We've now provided all of the code necessary to dynamically retrieve a student's information whenever he or she logs on to the SRS. We'll demonstrate how to simulate a logon in order to test this code when we talk about changes to the SRS class's Main method in a few moments. Before we do so, however, there is one more important enhancement that we wish to make to the Student class, having to do with persisting the results of a student's registration session.

Persisting the State of a Student

During the course of an SRS session, the student will presumably be registering for sections and/or dropping sections. The state of the Student object representing that student—namely the attribute values of the Student object and all links maintained by that Student object with other objects (in particular, with Section objects in which the student is enrolled)—is thus likely to change.

We must provide a way for the SRS to "remember" these changes from one logon session to the next; otherwise, the SRS system will be of no practical value. So, we're going to provide a method to persist a Student object's state whenever the student logs off: specifically, we're going to write information about this Student back out to the same data file that we originally used to initialize the Student object's state in the constructor.

  // This method writes out all of the student's information to
  // his/her ssn.dat file when he/she logs off.

  public bool Persist() {

We use the approach discussed earlier in the chapter of using a FileStream/StreamWriter combination to write to the student's data file.

    FileStream fs = null;
    StreamWriter sw = null;
    try {
      // Attempt to create the ssn.dat file.  Note that
      // it will overwrite one if it already exists, which
      // is what we want to have happen.

      string file = this.Ssn + ".dat";
      fs = new FileStream(file, FileMode.OpenOrCreate);
      sw = new StreamWriter(fs);

      // First, we output the header record as a  tab-delimited
      // record.

      sw.WriteLine(this.Ssn + "\t" + this.Name + "\t" +
                   this.Major + "\t" + this.Degree);

      // Then, we output one record for every Section that
      // the Student is enrolled in.

      for (int i = 0; i < attends.Count; i++) {
        Section s = (Section) attends[i];
        sw.WriteLine(s.GetFullSectionNo());
      }

      sw.Close();
    }

If anything went wrong during the persistence process (such as the file couldn't be opened or written to), the following catch block is triggered. In this case, we'll return a bool value of false to signal to the client code that something went wrong, and will allow the client code to determine what needs to be done as a result (whether some error message needs to be displayed to the user, or whether some alternative recovery mechanism needs to be engaged):

    catch (IOException e) {
      // Signal that an error has occurred.

      return false;
    }

    // All is well!
    return true;
  }
Note?/td>

Writing out the values of an object's fields to a file in ASCII record format is a "quick and dirty" approach to object serialization. The .NET Framework offers a variety of more sophisticated ways to persist objects to files. More detailed information on alternative serialization techniques can be found in the MSDN documentation pages.


Team LiB
Previous Section Next Section