Creating a Data File Programmatically

After that discussion of "errors, their cause and cure", let's get back to the program. The next thing we're going to add is a way for the user to enter, modify, and delete information for home inventory items without having to manually create or edit a data file.

Susan thought I had something against data files. I cleared up her confusion with the following discussion.

Susan: What's wrong with data files?

Steve: Nothing's wrong with them. What's wrong is making the user type everything in using a text editor; instead, we're going to give the user the ability to create the data file with a data entry function designed for that purpose.

Susan: Oh, so we're creating a database?

Steve: You could say that. Its current implementation is pretty primitive, but could be upgraded to handle virtually any number of items if that turned out to be necessary.

Let's start with the ability to enter data for a new object, as that is probably the first operation a new user will want to perform.

Figure 11.18 shows the new header file for the HomeInventory class, which includes the new AddItem member function.

Figure 11.18. The next interface for the HomeInventory class (codehmin4.h)
class HomeInventory
{
public:
  HomeInventory();

  short LoadInventory(std::ifstream& is);
  HomeItem FindItemByName(std::string Name);
  HomeItem AddItem();

private:
  Vec<HomeItem> m_Home;
};

How did I decide on the signature of the AddItem member function? Well, it seems to me that the result of this function should be the HomeItem that it creates. As for the arguments (or lack thereof), the data for the new item is going to come from the user of the program via the keyboard (i.e., cin), so it doesn't seem necessary to provide any other data to the function when it is called in the program.

Susan wanted to make sure she knew what “user” meant in this context.

Susan: Is that the end user or the programmer who is using the HomeItem class?

Steve: Good question. In this case, it's the end user of the program.

The implementation of this new function is shown in Figure 11.19.

Figure 11.19. The AddItem member function of HomeInventory (from codehmin4.cpp)
HomeItem HomeInventory::AddItem()
{
  HomeItem TempItem = HomeItem::NewItem();

  short OldCount = m_Home.size();

  m_Home.resize(OldCount + 1);

  m_Home[OldCount] = TempItem;

  return TempItem;
}

The first statement of this function, HomeItem TempItem = HomeItem::NewItem();, creates a new HomeItem object called TempItem.

Susan had some questions about that statement.

Susan: Why is TempItem a HomeItem?

Steve: Because that's the type of object we use to keep track of the items in our home inventory.

Susan: I don't get how HomeItem is a type. It should be an object.

Steve: A class defines a new type of object. A type like HomeItem could be compared to a common noun like “cat”, whereas the objects of that type resemble proper nouns like “Bonsai”. You wouldn't say that you have “cat”, but you might say that you have “a cat named Bonsai”. Similarly, you wouldn't say that your program has HomeItem, but that it has a HomeItem called (in this case) TempItem.[11]

[11] By an amazing coincidence, we actually do have a cat named Bonsai.

The initial value for TempItem is the return value of the call to HomeItem::NewItem();. The reason we have to specify the class of this function (HomeItem) is that it is a member function of the HomeItem class, not of the HomeInventory class. But what kind of function call is HomeItem::NewItem()? It obviously isn't a normal member function call because there's no object in front of the function name NewItem.

This is a static member function call. You may recall from Chapter 9 that a static member function is one for which we don't need an object. Our previous use of this type of function was in the Today function, which returns today's date; clearly, today's date doesn't depend on which object we are referring to. However, this type of member function is also convenient in cases such as the present one, where we are creating an object from keyboard input and therefore don't have the object available for use yet.

Before we get started on the implementation of the static member function called NewItem, let's take a look at the new interface for the HomeItem class, which is shown in Figure 11.20.

Figure 11.20. The new interface for HomeItem (codehmit4.h)
// hmit4.h

class HomeItem
{
friend std::ostream& operator << (std::ostream& os, const HomeItem& Item);
friend std::istream& operator >> (std::istream& is, HomeItem& Item);

public:
 HomeItem();
 HomeItem(const HomeItem& Item);
 HomeItem& operator = (const HomeItem& Item);
virtual ~HomeItem();

// Basic: Art objects, furniture, jewelry, etc.
 HomeItem(std::string Name, double PurchasePrice,
 long PurchaseDate, std::string Description, std::string Category);

// Music: CDs, LPs, cassettes, etc.
 HomeItem(std::string Name, double PurchasePrice,
 long PurchaseDate, std::string Description, std::string Category,
 std::string Artist, Vec<std::string> Track);

virtual void Write(std::ostream& os);
virtual void FormattedDisplay(std::ostream& os);
virtual std::string GetName();

static HomeItem NewItem();

protected:
 HomeItem(int);
protected:
 HomeItem* m_Worker;
 short m_Count;
};

We'll get to some of the changes between the previous interface and this one as soon as we get through with the changes in the implementation needed to allow data input from the keyboard. The first part of this implementation is the code for HomeItem::NewItem(), which is shown in Figure 11.21.

Figure 11.21. The implementation of HomeItem::NewItem() (from codehmit4.cpp)
HomeItem HomeItem::NewItem()
{
 HomeItem TempItem;

 cin >> TempItem;

 return TempItem;
}

As you can see, this is a very simple function, as it calls operator >> to do all the real work; however, I had to modify operator >> to make this possible. Susan wanted to know what was wrong with the old version of operator >>.

Susan: Why do we need another new version of operator >>? What was wrong with the old one?

Steve: The previous version of that operator wasn't very friendly to the user who was supposed to be typing data at the keyboard. The main problem is that it didn't tell the user what to enter or when to enter it; it merely waited for the user to type in the correct data.

I fixed this problem by changing the implementation of operator >> to the one shown in Figure 11.22.

Figure 11.22. The new version of operator >> (from codehmit4.cpp)
istream& operator >> (istream& is, HomeItem& Item)
{
  string Type;
  string Name;
  double PurchasePrice;
  long PurchaseDate;
  string Description;
  string Category;
  bool Interactive = (&is == &cin);

  while (Type == "")
   {
   if (Interactive)
     cout << "Type (Basic, Music) ";
   getline(is,Type);
   if (is.fail() != 0)
     {
     Item = HomeItem();
     return is;
     }
    }

 if (Interactive)
   cout << "Name ";
 getline(is,Name);

 if (Interactive)
   cout << "Purchase Price ";
 is >> PurchasePrice;

 if (Interactive)
   cout << "Purchase Date ";
 is >> PurchaseDate;
 is.ignore();
 if (Interactive)
   cout << "Description ";
 getline(is,Description);

 if (Interactive)
   cout << "Category ";
 getline(is,Category);

 if (Type == "Basic")
   {
   Item = HomeItem(Name, PurchasePrice, PurchaseDate,
        Description, Category);
   }
 else if (Type == "Music")
   {
   string Artist;
   short TrackCount;

   if (Interactive)
     cout << "Artist ";
   getline(is,Artist);

   if (Interactive)
     cout << "TrackCount ";
   is >> TrackCount;
   is.ignore();

   Vec<string> Track(TrackCount);
   for (short i = 0; i < TrackCount; i ++)
     {
     if (Interactive)
       cout << "Track # " << i + 1 << ": ";
     getline(is,Track[i]);
     }
   Item = HomeItem(Name, PurchasePrice, PurchaseDate,
         Description, Category, Artist, Track);
   }
 else
   {
   cout << "Can't create object of type " << Type << endl;
   exit(1);
   }

 return is;
}

I think most of the changes to this function should be fairly obvious. Assuming that we are reading data from the keyboard (i.e., Interactive is true), we have to let the user know what item we want typed in by displaying a prompt message such as “Name: ” or “Category: ”.

Susan had an excellent question about the implementation of this function.

Susan: How does it know whether input is from a file or the keyboard?

Steve: By testing whether the istream that we're reading from is the same as cin.

This test is performed by the statement

bool Interactive = (&is == &cin);

which defines a variable of type bool called Interactive that is initialized to the value of the expression (&is == &cin). What is the value of that expression going to be?

Comparing Two streams

The value is the result of applying the comparison operator, ==, to the arguments &is and &cin. As always with comparison operators, the type of this result is bool, which is why the type of the variable Interactive is bool as well. As for the value of the result, in this case the items being compared are the addresses of the variables cin and is. If you look back at the header for operator >>, you'll see that is is a reference argument that is actually just another name for the istream being supplied as the left-hand argument to the operator >> function. Therefore, what we are testing is whether the istream that is the left-hand argument to operator >> has the same address as cin has (i.e., whether they are different names for the same variable); if so, we are reading data from the keyboard and need to let the user know what we want. Otherwise, we assume the data is from a file, so there is no need to prompt the user.

You may wonder why we have to compare the addresses of these two variables and not simply their contents — that is, why we have to write (&is == &cin) instead of (is == cin). The reason is that the expression (is == cin) compares whether the two streams is and cin are in the same “state”; i.e., whether both (or neither) are available for use, not whether they are the same stream. If this isn't obvious to you, you're not alone. Not only did I not figure it out right away but when I asked a C++ programmer with over 25 years of experience in the language, he took two tries to decipher it.

Once that bit of code is clear, the rest of the changes should be pretty obvious, as they consist of the code needed to display prompts when necessary. For example, the sequence:

if (Interactive)
  cout << "Type (Basic, Music) ";

writes the line “Type (Basic, Music)” to the screen if and only if the input is from cin — that is, if the user is typing at the keyboard. The other similar sequences do the same thing for the other data items that need to be typed in.

Susan still wasn't convinced of the necessity to rewrite operator >>, but I think I won her over.

Susan: But why should we have to change operator >> in the first place? Why not just write a separate function to read the data from the keyboard and leave operator >> as it was? Isn't object-oriented programming designed to allow us to reuse existing functions rather than modify them?

Steve: Yes, but it's also important to try to minimize the number of functions that perform essentially the same operation for the same data type. In the current case, my first impulse was to write a separate function so I wouldn't have to add all those if statements to operator >>. However, I changed that plan when I realized that such a new function would have to duplicate all of the data input operations in operator >>. This means that I would have to change both operator >> and this new function every time I changed the data for any of the HomeItem classes. Since this would cause a maintenance problem in future updates of this program, I decided that I would just have to put up with the if statements.

Susan: Why would this cause a maintenance problem?

Steve: If we have more than one function that shares the same information, we have to locate all such functions and change them whenever that shared information changes. In this case, the shared information is embodied in the code that reads values from an istream and uses those values to create a HomeItem object. Therefore, if we were to change the information needed to create a HomeItem object, we would have to find and change every function that created such an object. In a large program, just finding the functions that were affected could be a significant task.

Another reason to use the same function for both keyboard and file input is that it makes the program easier to read if we use the same function (e.g., operator >>) for similar operations.

Besides changing operator >>, I've added a couple of new functions to the interface for HomeItem — namely, FormattedDisplay and GetName. The first of these functions, as usual for virtual functions declared in the interface of a polymorphic object, simply uses the virtual function mechanism to call the “real” FormattedDisplay function in the object pointed to by m_Worker. The code in the versions of this function in the HomeItemBasic and HomeItemMusic classes is almost identical to the code for Write, except that it adds an indication of what each piece of data represents. Knowing that, you shouldn't have any trouble following either of these functions, so I'm going to list them without comment in Figures 11.23 and 11.24.

Figure 11.23. HomeItemBasic::FormattedDisplay (from codehmit4.cpp)
void HomeItemBasic::FormattedDisplay(ostream& os)
{
 os << "Type: ";
 os << GetType() << endl;
 os << "Name: ";
 os << m_Name << endl;
 os << "Purchase price: ";
 os << m_PurchasePrice << endl;
 os << "Purchase date: ";
 os << m_PurchaseDate << endl;
 os << "Description: ";
 os << m_Description << endl;
 os << "Category: ";
 os << m_Category << endl;
}

Figure 11.24. HomeItemMusic::FormattedDisplay (from codehmit4.cpp)
void HomeItemMusic::FormattedDisplay(ostream& os)
{
 HomeItemBasic::FormattedDisplay(os);

 os << "Artist: ";
 os << m_Artist << endl;
 os << "Tracks: ";

 int TrackCount = m_Track.size();
 os << TrackCount << endl;
 for (short i = 0; i < TrackCount; i ++)
   os << m_Track[i] << endl;
}

Now that we've looked at FormattedDisplay, what about GetName? You might think that this is about as simple as a function can get, as its sole purpose is to return the value of the m_Name member variable. We'll see shortly how this function is used in the test program. Before we look at that, though, you should note that this function has a characteristic that we haven't run across before. It is a virtual function implemented in HomeItem and HomeItemBasic but not in HomeItemMusic. Why is this, and how does it work?

Inheriting the Implementation of a virtual Function

The answer is that just as with a non-virtual function, if we don't write a new version for a derived class (in this case, HomeItemMusic), the compiler will assume that we are satisfied with the version in the most recently defined base class (in this case, HomeItemBasic). Since the correct behavior of GetName (returning the value of m_Name) is exactly the same in HomeItemMusic as it is in HomeItemBasic, there's no reason to write a new version of GetName for HomeItemMusic, and therefore we won't.

Figure 11.25 shows how we can use these functions to add an item from the keyboard. After adding the item to the inventory, this test program retrieves its name via the GetName function, uses FindItemByName to look it up by its name, and displays it. Just to make sure our new operator >> still works for file input, this test program also loads the inventory from an input file and displays one of the elements read from that file, as the previous test program did. Making sure we haven't broken something that used to work is called regression testing, and it's a very important part of program maintenance.[12]

[12] In fact, operator >> did not work correctly for file input the first time I tried it because I had made the mistake of testing the equality of the istreams is and cin rather than the equality of their addresses, as mentioned previously. So it's a good thing I thought to check that use of operator >>!

Figure 11.25. The test program for adding a HomeItem interactively (hmtst4.cpp)
#include <iostream>
#include <fstream>

#include "Vec.h"
#include "hmit4.h"
#include "hmin4.h"
using namespace std;

int main()
{
   ifstream HomeInfo("home3.in");
   HomeInventory MyInventory;
   HomeItem TempItem;
   string Name;

   MyInventory.LoadInventory(HomeInfo);

   TempItem = MyInventory.AddItem();
   Name = TempItem.GetName();
   HomeItem test2 = MyInventory.FindItemByName(Name);
   cout << endl << "Here is the item you added" << endl;
   test2.FormattedDisplay(cout);

   HomeItem test1 = MyInventory.FindItemByName("Relish");
   cout << endl << "Here is an item from the file" << endl;
   test1.FormattedDisplay(cout);

   return 0;
}

Now we can add a new item and retrieve it, so what feature should we add next? A good candidate would be a way to make changes to data that we've already entered. We will call this new function of the Inventory class EditItem, to correspond to our AddItem function. Let's look at the new interface of the HomeInventory class, which is shown in Figure 11.26.

Figure 11.26. The next version of the interface for HomeInventory (codehmin5.h)
//hmin5.h

class HomeInventory
{
public:
  HomeInventory();

  short LoadInventory(std::ifstream& is);
  void DumpInventory();
  HomeItem FindItemByName(std::string Name);
  HomeItem AddItem();
  short LocateItemByName(std::string Name);
  HomeItem EditItem(std::string Name);

private:
  Vec<HomeItem> m_Home;
};

You may have noticed that I've added a couple of other support functions besides the new EditItem function. These are DumpInventory, which just lists all of the elements in the m_Home Vec (useful in debugging the program), and LocateItemByName, which we'll cover in the discussion of EditItem.

Susan had a question about the first of these support functions.

Susan: Why do we want to get rid of items with DumpInventory?

Steve: We don't want to. “Dump” is programming slang for “display without worrying about formatting”. In other words, a dump function is one that gives “just the facts”.

Figure 11.27 shows the test program for this new version of the home inventory application.

Figure 11.27. The next version of the HomeInventory test program (codehmtst5.cpp)
#include <iostream>
#include <fstream>

#include "Vec.h"
#include "hmit5.h"
#include "hmin5.h"
using namespace std;

int main()
{
   ifstream HomeInfo("home3.in");
   HomeInventory MyInventory;
   HomeItem TempItem;
   string Name;

   MyInventory.LoadInventory(HomeInfo);

   TempItem = MyInventory.FindItemByName("Relish");
   cout << endl;

   TempItem.Edit();
   cout << endl;

   TempItem.FormattedDisplay(cout);
   cout << endl;

   return 0;
}

The test program hasn't gotten much more complicated, as you can see; it loads the inventory, uses the new EditItem function to modify one of the items, and then displays the changed item.

Susan had a couple of good questions about this program and some comments about software development issues.

Susan: What happens if the program can't find the object that it's trying to look up?

Steve: That's a very good question. In the present case, that should never happen because the input file does have a record whose name is “Relish”. However, we should handle that possibility anyway by checking whether the returned item is null. We'll do so in a later version of the test program; the discussion of that issue is on page 890.

Susan: I don't see why we need to write a whole new function called LocateItemByName. What's wrong with the one we already have, FindItemByName?

Steve: Because FindItemByName returns a copy of the object but doesn't tell us where it came from in the m_Home Vec. Therefore, if we used that function we wouldn't be able to put the object back when we were finished editing it.

Susan: Why can't we leave the classes alone? It's annoying to have to keep changing them all the time.

Steve: I'm afraid that's the way software development works. Of course, I could make it more realistic by playing the role of a pointy-haired manager hovering over you while you're working.

Susan: No, thanks. I believe you.

Steve: Okay, we'll save that for our future management book, Programmers Are from Neptune, Managers Are from Uranus.

Susan: I can't wait.

After that bit of comic relief, let's take a look at the implementation of the new EditItem function, shown in Figure 11.28.

Figure 11.28. The EditItem function of HomeInventory (from codehmin5.cpp)
HomeItem HomeInventory::EditItem(string Name)
{
  short ItemNumber = LocateItemByName(Name);

  HomeItem TempItem = m_Home[ItemNumber];

  TempItem.Edit();

  m_Home[ItemNumber] = TempItem;

 return TempItem;
}

As you can see, this function isn't too complicated either. It calls LocateItemByName to find the element number of the Homeitem to be edited, copies that HomeItem from the m_Home Vec into a temporary HomeItem called TempItem, calls the Edit function (which we'll get to shortly) for that temporary HomeItem, and then copies the edited HomeItem back into the same position in the m_Home Vec. If you compare this function with AddItem (Figure 11.19 on page 794), you will notice a couple of differences. First, this function calls LocateItemByName rather than FindItemByName. These two functions are exactly the same, except that LocateItemByName returns the element number of the found HomeItem in the m_Home Vec rather than the HomeItem itself. This allows us to update the m_Home Vec with the edited HomeItem when we are through editing it. Second, the call to Edit in this function is different from the call to NewItem in AddItem because Edit has an object to operate on whereas NewItem had to create a previously nonexistent object. Therefore, Edit is a normal (non-static) member function rather than a static member function like AddItem.

What about the implementation of this new Edit function? I have good news and bad news. The good news: Using it is pretty simple. The bad news: Implementing it led to a fairly extensive revision of the HomeItem classes. I think the results are worth the trouble; hopefully, you will come to the same conclusion when we are done. Let's start with the new interface for the HomeItem class.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset