Case Study: The Bug Tracking Program Revisited

It is often necessary to revisit an old program and consider how you can improve it. However, this can be a frustrating exercise because the new specifications often can't be easily implemented by using the old scheme, and in such cases a redesign and rewrite is the only viable choice. There is too much bad code in the world already, kept beyond its natural life like some Frankenstein monster.

The first version of the bug-tracking program you created in Chapter 2, “Functions and Control Statements,” works, but users are frustrated: They need to track the people reporting and working on the bugs. They would prefer a number of small utility programs that they can run from the command prompt. Also, the date needs to be logged when a bug has been fixed. To implement these changes, you would just need to add extra fields. But the users also want to edit information in the bugs database—for example, they want to be able to add a comment or decide that a particular must-fix bug can be downgraded to a fix-whenever bug. This is not easy to do with a text file because you would have to rewrite the file for each tiny change. You decide to see how a binary file solution would work. This means the old database will have to be converted, but that would have been necessary anyway. It also raises another question: Can future changes be added without breaking compatibility?

Binary Files: Both Readable and Writable

Consider how the previous bug-tracking system remembered what the last ID was: It would read the old value from a small text file, increment it, and write it back. Here is one way to do this with binary files (note how you combine more than one ios constant to make the file readable and writable):


int next_id()
{
  int val;
  ifstream in(IDFILE, ios::binary | ios::in | ios::out);
  in.read((char *)&val,sizeof(int));
  ++val;
  // rewind to beginning of file
  in.seekg(0);
  in.write((char *)&val,sizeof(int));
  return val;
}

The significance of this for bug reports is that you can move around a binary file and overwrite existing data. So you need to define a suitable structure that describes a bug report. Then to move to the nth record, you need to seek (n-1)*sizeof(S) bytes, where S is the struct. If S is 100 bytes in size, then the offsets are 0, 100, 200, and so on.

What About the Users?

Each bug report has two associated users: the one that reported the bug and the one that signed off the bug as fixed. There are two corresponding dates—the reported date and the fixed date—which are easy to represent with the Date struct. But how do you represent users? Each user has an eight-character username (for example, SDONOVAN), but it seems a waste to copy the whole name into the structure. Besides, what if the username changes and is no longer eight characters? One solution is to store a user ID, which is a simple integer from 1 to the number of users. To translate from a username to a user ID, you use a map, and to translate from the ID to the name, you use an array. But the rest of the program doesn't need to know that. All the clients of the module need to know is that Users::name() translates IDs to names, and Users::id() translates names to IDs; if you later change the representation (say user IDs are no longer consecutive), the rest of the program is not broken. This interface is contained in users.h and looks like the following:


#include <string>
#include <map>
using namespace std;

#include "users.h"
namespace {  // private to this module...
 const int MAX_USERS = 20;
 typedef map<string,int> UserNameMap;
 UserNameMap users;
 string user_ids[MAX_USERS];
}

namespace Users {

// ids start at 1, not 0!
string name(int id) {  return user_ids[id-1];  }
int id(string s)    {  return users[s]; }
string myself()     {  return getenv("USERNAME");  }

void add(string s, int id)
{
  users[s] = id;
  user_ids[id-1] = s;
}
void init()
{
  add("GSMITH",1);
  add("FJONES",2);
  add("SREDDY",3);
  add("PNKOMO",4);
  add("SDONOVAN",5);
}

}  // namespace users

// users.h
// Interface to the users module
typedef int User;
const int MAX_USERS = 20;

namespace User {
 string name(int id);
 int    id(string s);
 string myself();
 void   add(string s, int id);
 void   init();
}

This approach to storing users avoids the problem of how to write strings to a file, but you can't get away from storing the actual bug description.

Writing Strings

The following is a structure that contains the required information:

const int MAX_DESCRIPTION = 80;

struct Report {
   int id,level;
   Date date_reported, date_fixed;
   User who_reported, who_fixed;
   long extra1;
   long extra2;
   int  descript_size;
   char description[MAX_DESCRIPTION];
};

In this example, everything is straightforward (User is just an integer) except the description, which is a fixed array of characters. There are also two extra fields, which allow some room for expansion. The problem with this example is that there is a limited amount of space for the description. 80 characters is probably enough for most bugs, but occasionally bugs need more description than that.

Let's first handle the issue of saving strings as character arrays. Generally, if you are going to do anything nontrivial to a structure and you want to keep your options open, it is a good idea to write functions that operate on the structure. You can wrap up the dark secrets of how strings are stored in two functions, which you can improve later. These are shown in the following code.

string get_description(const Report& report)
{
  return report.description;
}

void set_description(Report& report, string descript)
{
 report.descript_size = descript.size();
 strncpy(report.description, descript.c_str(), MAX_DESCRIPTION);
}

The function get_description() seems fairly trivial, but quite a bit goes on under the hood. The fixed-length character array is implicitly converted into a variable-length string. set_description() requires a useful function from the C runtime library. strncpy(dest,src,n) copies at most n characters from src to dest (note the direction). The string method c_str() provides the raw character data of the string, which is then copied into the report description field. If there are too many characters in the string, the description is truncated. (As usual, it is very important not to run over the end of the array.) The copy operation should be familiar to a C programmer, but this operation is usually conveniently (and safely) handled by C++ strings. (For more information on C-style string handling, see “The C String Functions” in Appendix B, “A Short Library Reference.”)

Adding a bug report is straightforward. You can define a shortcut for the common case of adding a dated report by using the current username. You don't have to bother to set the date_fixed field because the who_fixed member is set to zero, and this will mean “still pending, not fixed yet”:

void add_report(string who, int level, Date date,
                string descript)
{
  Report report;
  // fill in the report structure
  report.id = next_id();
  report.level = level;
  report.date_reported = date;
  set_description(report,descript);
  report.who_reported = Users::id(who);
  report.who_fixed = 0;  // meaning, not fixed yet!

  ofstream out(BUGFILE,ios::binary | ios::app);
  out.write((char *)&report,sizeof(Report));
}

void add_report_today(int level, string descript)
{
  add_report(Users::myself(),level,Dates::today(),descript);
}

The following are the routines for reading through the bug report file and printing reports out if they match the criteria:


void dump_report(ostream& out, const Report& report)
{
    out << report.id << ' ' << report.level
        << ' ' << Users::name(report.who_reported)
        << ' ' << Dates::as_str(report.date_reported)
        << ' ' << get_description(report) << endl;
}

void list_reports(ostream& out, int min_level, Date d1, Date d2)
{
  Report report;
  ifstream in(BUGFILE, ios::binary | ios::in);
  while (in.read((char *)&report,sizeof(Report)))
    if (report.level > min_level &&
             Dates::within_range(report.date_reported,d1,d2))
      dump_report(out,report);
}

bool read_report(int id, Report& rpt)
{
  ifstream in(BUGFILE, ios::binary | ios::in);
  in.seekg((id-1)*sizeof(Report));
  if (!in.eof()) {
       in.read((char *)&rpt, sizeof(Report));
       return true;
  }  else return false;
}

In this example, you separate out the code that does the dumping to simplify list_reports(). As you can see, it is considerably simpler than the equivalent function using text files; the whole structure is read by using the read() method, which returns false when the end of the bugs file is reached. The function read_report() returns a report from the file, given its ID. Assuming that the IDs are consecutive integers, it is straightforward to go to exactly the position where you can read the requested report.

The code for write_report() is very similar to the code for read_report(), except that you use seekp() rather than seekg() and write() rather than read(). (See chap5ugs.cpp on the CD-ROM that accompanies this book.) Being able to modify a bug report is a powerful feature; with the old text file representation, you would have had to rewrite the file. The question of deleting bug reports doesn't occur with this version of the bug tracker, because even a false report must be dealt with and written off as “not-a-bug” and kept for future reference. But if you did want to delete bug reports, it would be easier to mark them as being deleted than to rewrite them each time. This technique is often used in implementing databases; at some future point, you compact the database, which actually removes all the collected garbage by leaving out records marked as deleted.

The Utility Program Interface

The prototypes of the functions in bugs.cpp are collected in bugs.h, and you can now pull this together as a program:


// addbg.cpp
#include <iostream>
#include <cstdlib>
using namespace std;

#include "bugs.h"

int main(int argc, char **argv)
{
// remember, argc counts argv[0] as well!
  if (argc != 3) {
      cerr << "usage: <level> <description>
"
           << "Adds a bug report to the database
";
      return -1;        // this program failed!
  }
  int level = atoi(argv[1]);
  if (level == 0) {
      cerr << "Bug level must be > 0
";
      return -1;
  }
  Bugs::add_report_today(level, argv[2]);
  return 0;  // success
}

It is considered bad manners for programs to crash because of bad input, so you should try to give users sensible error messages.

To build this program, you have to compile all the source files and link them together. Initially, the following DOS command will compile all the files, and subsequently you have to recompile only the files that change. Refer to Appendix D, “Compiling C++ Programs and DLLs with GCC and BCC32,” for the options, including setting up a project in an integrated development environment such as Quincy 2000.

C:olshucwchap5> c++ addb.cpp bugs.cpp users.cpp dates.cpp - o addbg.exe
						

Extensions

Let's now work on the case where the description is more than MAX_DESCRIPTION characters. Fortunately, there are two extra fields available; these fields cannot save more than eight characters, but one of them can give an offset into another file. That is, if there are too many characters, you can append them to the end of a file of strings and record the position in the extra1 member of the struct. There is some redundancy because you save the first MAX_DESCRIPTION characters twice—in the bug report file and also in the file of strings—but that would not be difficult to improve. The following code shows new definitions for set_description() and get_description().

Writing out the description to the strings file is straightforward because you aren't forcing the string into a fixed-length block of characters. The string's raw character data can be written directly to the file as so many bytes. You can 'tell' what the offset at the end of the file is by using tellg(). Before you write, you save this offset.

Reading the string back involves moving to the stored offset with seekg() and reading descript_size bytes into a temporary buffer, which is then returned as a string. get_description() declares this buffer as static because sometimes too much local storage can cause a stack overflow.


void set_description(Report& report, string descript)
{
 int sz = descript.size();
 report.descript_size = sz;
 strncpy(report.description, descript.c_str(), MAX_DESCRIPTION);
 // extra long description?
 if (sz > MAX_DESCRIPTION) {
   ofstream out(STRSFILE,ios::binary | ios::app);
   report.extra1 = out.tellp();
   out.write(descript.c_str(),sz);
 }
}

string get_description(const Report& report)
{
 if (report.descript_size < MAX_DESCRIPTION)
      return report.description;
 else {  // read the description from the strings file
   static char buffer[TRULY_MAX_DESCRIPT];
   ifstream in(STRSFILE,ios::binary);
   in.seekg(report.extra1);
   in.read(buffer,report.descript_size);
   return buffer;
 }
}

Note that you could make these modifications to the program without having to change the rest of the program because the description reading and writing logic has been separated out. That is, you are just replacing set_description() and get_description().

Later in this book, we'll explore this issue in greater detail, but it's useful to note that object-oriented programming is more of a mental habit than using object-oriented language features like classes. With object-oriented programming, you need to use encapsulation (that is, wrap up tricky logic and assumptions into functions, which are given exclusive right to manage some data). It is possible to write object-oriented programs in any language that has structures and functions. And conversely, you could write incoherent, bad code and dress it up in classes. I will introduce the object-oriented features of C++ like classes in Chapter 7, “Classes.”

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

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