Team LiB
Previous Section Next Section

CourseCatalog

We'll start with the CourseCatalog class. As mentioned earlier, we'll treat it as an encapsulated collection by adding one feature, an attribute named courses declared to be of type Hashtable that will be used to hold Course object references.

using System;
using System.Collections;
using System.IO;

public class CourseCatalog : CollectionWrapper {
  //------------
  // Attributes.
  //------------
  // This Hashtable stores Course object references, using
  // the (string) course no. of the Course as the key.

  private Hashtable courses;

Constructor

The constructor for this class is fairly trivial:

  public CourseCatalog() {
    // Instantiate a  new Hashtable.

    courses = new Hashtable();
  }

and no properties are declared in the CourseCatalog class.

Display Method

We create a Display method for testing purposes, which uses an IDictionaryEnumerator object to step through the table, a technique that we discussed in Chapter 13.

  // Used for testing purposes.

  public void Display() {
    Console.WriteLine("Course Catalog:");
    Console.WriteLine("");

    // Step through the Hashtable and display all entries.

    IDictionaryEnumerator e = courses.GetEnumerator();

    while (e.MoveNext()) {
     Course c = (Course) e.Value;
     c.Display();
     Console.WriteLine("");
    }
  }

AddCourse Method

We also create an AddCourse "housekeeping" method, which is used to insert a Course object reference into the encapsulated collection:

  public void AddCourse(Course c) {
    // We use the course no. as the key.
    string key = c.CourseNo;
    courses.Add(key, c);
  }

ParseData Method

In order to be able to instantiate the CourseCatalog class, we must make it concrete by providing a method body for the abstract ParseData and ParseData2 methods that we've inherited from CollectionWrapper. We'll start with ParseData, which will be used to read the CourseCatalog.dat file.

Because this is a fairly complex method, we'll present the code in its entirety first, and will then narrate it after the fact.

  public override void ParseData(string line) {
    // We're going to parse tab-delimited records into
    // three attributes -- courseNo, courseName, and credits --
    // and then call the Course constructor to fabricate a  new
    // course.

    // 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 assign a  value to each of three local string variables
    // using the appropriate substring.

    string courseNo = strings[0];
    string courseName = strings[1];
    string creditValue = strings[2];

    // We have to convert the last value into a  number,
    // using a  static method on the Double class to do so.

    double credits = Convert.ToDouble(creditValue);

    // Finally, we call the Course constructor to create
    // an appropriate Course object, and store it in our
    // collection.

    Course c = new Course(courseNo, courseName, credits);
    AddCourse(c);
  }

We'll now narrate selected portions of the code.

The System.String class defines the Split method, which is used to split a string value into substrings based on a programmer-specified delimiter, storing the resultant substrings in a string array. In our case, we've designated the tab character, '\t', as the delimiter.

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

As an example, if we call the Split method on the following line/string (where <tab> indicates the presence of an otherwise invisible tab character):

CMP101<tab>Beginning Computer Technology<tab>3.0

then the resultant string array would contain three elements, "CMP101", "Beginning Computer Technology", and "3.0". Since we know that the first substring will be the course number, the second will be the course name, and the third will be the number of credits for the course, we can assign the value of each substring to an appropriate local variable:

    string courseNo = strings[0];
    string courseName = strings[1];
    string creditValue = strings[2];

We now have three string variables—courseNo, courseName, and creditValue— correctly parsed. But, there's one small problem: the Course constructor, shown here:

  public Course(string cNo, string cName, double credits) { ... }

expects to be handed a double value for the credit value of the course, not a string value. We therefore must convert the string stored in variable creditValue into a double value. Fortunately, the Convert class in the System namespace provides a number of static methods for converting one type into another. We'll use the ToDouble method to convert the string into a double:

    double credits = Convert.ToDouble(creditValue);

We're finally ready to call the Course constructor to create an appropriate Course object, and to store it in our collection by calling the AddCourse method:

    Course c = new Course(courseNo, courseName, credits);
    AddCourse(c);
  }

Please note that we've provided virtually no error checking in the ParseData method. We're assuming that the CourseCatalog.dat file is perfectly formatted, which is a risky assumption in real life! One of the exercises at the end of this chapter will give you a chance to make this code more robust.

FindCourse Method

We also provide a convenience method, FindCourse, to enable client code to easily retrieve a particular Course object from this collection based on its course number. If the requested course number isn't found, the value null will be returned by this method:

  public Course FindCourse(string courseNo) {
    return (Course) courses[courseNo];
  }

Providing such a method hides the fact that the collection is implemented as a Hashtable: client code simply invokes the method and gets handed back a Course object, without any idea as to what is happening behind the scenes, as simulated by the following hypothetical client code "snippet":

// Sample client code.
CourseCatalog courseCatalog = new CourseCatalog();
// ...
Course c = courseCatalog.FindCourse("ART101");

ParseData2 Method

We're not quite done yet. We must read a second file, Prequisites.dat, which defines course prerequisites, so that we may properly link our newly created Course objects together. (We have to do this as a separate second step after all Course objects have been created via the ParseData method so that we don't wind up with a "chicken vs. egg" situation—namely, needing to link together two Courses that don't both exist yet.)

So, let's now provide a body for the abstract ParseData2 method. Because the logic of the ParseData2 method is so similar to the ParseData method, we won't discuss all of its logic in detail, but will point out one interesting "twist":

  public override void ParseData2(string line) {
    // We're going to parse tab-delimited records into
    // two values, representing the courseNo "A" of
    // a  course that serves as a  prerequisite for
    // courseNo "B".

    // Once again we'll make use of the Split method to split
    // the line into substrings using tabs as the delimiter

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

    // Now assign the value of the substrings to the
    // appropriate local string variables.

    string courseNoA = strings[0];
    string courseNoB = strings[1];

Because we wish to link together two preexisting Course objects via the AddPrerequisite method of the Course class, we have to obtain handles on these objects. We do so by using our FindCourse method to look up the two course numbers that we've just parsed. Only if we're successful in finding both Courses in our internal Hashtable—that is, only if both Course references a and b have non-null values—will we invoke the AddPrerequisite method on b, passing in a reference to a:

    // Look these two courses up in the CourseCatalog.

    Course a  = FindCourse(courseNoA);
    Course b = FindCourse(courseNoB);
    if (a != null && b != null) {
      b.AddPrerequisite(a);
    }
  }

Adding a "Test Scaffold" Main Method

In Chapter 13, we discussed the fact that the C# runtime environment looks for a method with a particular header, e.g., static void Main(), when we start up the application. As an example, to run the SRS application we developed in Chapter 14, we'd type the command: SRS, and the C# runtime environment would invoke the Main method declared within the SRS.cs file.

As we've discussed previously in the book, it's permissible to have more than one Main method sprinkled throughout an application's classes; however, only one of these will serve as the official main method for purposes of driving the application. Why would we ever want to declare extra Main methods? That is, what would we use the other Main methods for? As test drivers, or "scaffolds," for putting a single class through its paces. As an example, perhaps after we finish coding the CourseCatalog class, we wish to test it to ensure that it

  • Properly parses the CourseCatalog.dat and Prerequisites.dat files

  • Instantiates all Course objects correctly

  • Links prerequisites appropriately

  • Stores them all in the courses Hashtable that is encapsulated within the CourseCatalog class

We could run the full-blown SRS application to test this class. However, we'd have to jump ahead and modify the SRS class's Main method to take advantage of the CourseCatalog class and all of its various methods in order to do this. A simpler approach is to provide the CourseCatalog class with its own Main method, for use in testing the class in isolation, as follows:

  // Test scaffold (a Main() method INSIDE OF the CourseCatalog class!).

  static void Main() {
    // We instantiate a  CourseCatalog object ...

    CourseCatalog cc = new CourseCatalog();

    // ... and cause it to read both the CourseCatalog.dat and
    // Prerequisites.dat files, thereby testing both
    // the ParseData() and ParseData2() methods internally
    // to the InitializeObjects() method ...

    cc.InitializeObjects("CourseCatalog.dat", true);
    cc.InitializeObjects("Prerequisites.dat", false);

    // ... then use its Display() method to demonstrate the
    // results!

    cc.Display();
  }

With the addition of this Main method to class CourseCatalog, we can now compile the application from the command line using the following command:

csc /out:CourseCatalog.exe *.cs /main:CourseCatalog

As we've discussed previously, the /out compiler option tells the compiler to name the resulting executable file CourseCatalog.exe. The /main option signifies that the Main method defined in the CourseCatalog class will be used to drive the program. If the CourseCatalog.exe file is run, the following output would be produced as a result of the invocation of cc.Display():

Course Catalog:

Course Information:
  Course No.:  CMP101
  Course Name:  Beginning Computer Technology
  Credits:  3.0
  Prerequisite Courses:
    (none)
  Offered As Section(s):

Course Information:
  Course No.:  CMP283
  Course Name:  Higher Level Languages (C#)
  Credits:  3.0
  Prerequisite Courses:
    OBJ101:  Object Methods for Software Development
  Offered As Section(s):

Course Information:
  Course No.:  CMP999
  Course Name:  Living Brain Computers
  Credits:  3.0
  Prerequisite Courses:
    CMP283:  Higher Level Languages (C#)
  Offered As Section(s):

Course Information:
  Course No.:  ART101
  Course Name:  Beginning Basketweaving
  Credits:  3.0
  Prerequisite Courses:
    (none)
  Offered As Section(s):

Course Information:
  Course No.:  OBJ101
  Course Name:  Object Methods for Software Development
  Credits:  3.0

  Prerequisite Courses:
    CMP101:  Beginning Computer Technology
  Offered As Section(s):

thus demonstrating that our code does indeed work!

Note that there is no harm in leaving this Main method in the CourseCatalog class even after our testing is finished—in fact, it's a handy thing to keep around in case we change the details of how any of these methods work later on, and want to retest it.


Team LiB
Previous Section Next Section