- •Introduction
- •Saving Time with This Book
- •Conventions Used in This Book
- •Part II: Working with the Pre-Processor
- •Part III: Types
- •Part IV: Classes
- •Part V: Arrays and Templates
- •Part VI: Input and Output
- •Part VII: Using the Built-in Functionality
- •Part VIII: Utilities
- •Part IX: Debugging C++ Applications
- •Part X: The Scary (or Fun!) Stuff
- •Icons Used in This Book
- •Creating and Implementing an Encapsulated Class
- •Creating a Mailing-List Application
- •Testing the Mailing-List Application
- •Customizing a Class with Polymorphism
- •Testing the Virtual Function Code
- •Why Do the Destructors Work?
- •Delayed Construction
- •The cDate Class
- •Testing the cDate Class
- •Creating the Header File
- •Testing the Header File
- •The Assert Problem
- •Fixing the Assert Problem
- •Using the const Construct
- •Identifying the Errors
- •Fixing the Errors
- •Fixing What Went Wrong with the Macro
- •Using Macros Appropriately
- •Using the sizeof Function
- •Evaluating the Results
- •Using sizeof with Pointers
- •Implementing the Range Class
- •Testing the Range Class
- •Creating the Matrix Class
- •Matrix Operations
- •Multiplying a Matrix by a Scalar Value
- •Multiplying a Matrix by Scalar Values, Take 2
- •Testing the Matrix Class
- •Implementing the Enumeration Class
- •Testing the Enumeration Class
- •Implementing Structures
- •Interpreting the Output
- •Defining Constants
- •Testing the Constant Application
- •Using the const Keyword
- •Illustrating Scope
- •Interpreting the Output
- •Using Casts
- •Addressing the Compiler Problems
- •Testing the Changes
- •Implementing Member-Function Pointers
- •Updating Your Code with Member-Function Pointers
- •Testing the Member Pointer Code
- •Customizing Functions We Wrote Ourselves
- •Testing the Default Code
- •Fixing the Problem
- •Testing the Complete Class
- •Implementing Virtual Inheritance
- •Correcting the Code
- •Rules for Creating Overloaded Operators
- •Using Conversion Operators
- •Using Overloaded Operators
- •Testing the MyString Class
- •Rules for Implementing new and delete Handlers
- •Overloading new and delete Handlers
- •Testing the Memory Allocation Tracker
- •Implementing Properties
- •Testing the Property Class
- •Implementing Data Validation with Classes
- •Testing Your SSN Validator Class
- •Creating the Date Class
- •Testing the Date Class
- •Some Final Thoughts on the Date Class
- •Creating a Factory Class
- •Testing the Factory
- •Enhancing the Manager Class
- •Implementing Mix-In Classes
- •Testing the Template Classes
- •Implementing Function Templates
- •Creating Method Templates
- •Using the Vector Class
- •Creating the String Array Class
- •Working with Vector Algorithms
- •Creating an Array of Heterogeneous Objects
- •Creating the Column Class
- •Creating the Row Class
- •Creating the Spreadsheet Class
- •Testing Your Spreadsheet
- •Working with Streams
- •Testing the File-Reading Code
- •Creating the Test File
- •Reading Delimited Files
- •Testing the Code
- •Creating the XML Writer
- •Testing the XML Writer
- •Creating the Configuration-File Class
- •Setting Up Your Test File
- •Building the Language Files
- •Creating an Input Text File
- •Reading the International File
- •Testing the String Reader
- •Creating a Translator Class
- •Testing the Translator Class
- •Creating a Virtual File Class
- •Testing the Virtual File Class
- •Using the auto_ptr Class
- •Creating a Memory Safe Buffer Class
- •Throwing and Logging Exceptions
- •Dealing with Unhandled Exceptions
- •Re-throwing Exceptions
- •Creating the Wildcard Matching Class
- •Testing the Wildcard Matching Class
- •Creating the URL Codec Class
- •Testing the URL Codec Class
- •Testing the Rot13 Algorithm
- •Testing the XOR Algorithm
- •Implementing the transform Function to Convert Strings
- •Testing the String Conversions
- •Implementing the Serialization Interface
- •Creating the Buffer Class
- •Testing the Buffer Class
- •Creating the Multiple-Search-Path Class
- •Testing the Multiple-Search-Path Class
- •Testing the Flow Trace System
- •The assert Macro
- •Logging
- •Testing the Logger Class
- •Design by Contract
- •Adding Logging to the Application
- •Making Functions Inline
- •Avoiding Temporary Objects
- •Passing Objects by Reference
- •Choosing Initialization Instead of Assignment
- •Learning How Code Operates
- •Testing the Properties Class
- •Creating the Locking Mechanism
- •Testing the Locking Mechanism
- •Testing the File-Guardian Class
- •Implementing the Complex Class
- •Creating the Conversion Code
- •Testing the Conversion Code
- •A Sample Program
- •Componentizing
- •Restructuring
- •Specialization
- •Index
Restructuring 451
{
sWord += in[i];
}
}
if ( sWord.length() )
words.insert( words.end(), sWord );
return true;
}
int num()
{
return words.size();
}
string word( int idx )
{
string s = “”;
if ( idx < 0 || idx > (int)words.size()-1 )
return s;
s = words[idx]; return s;
}
};
int main(int argc, char **argv )
{
ParserFile pf( “myfile.txt” ); Parser p(‘:’);
while ( !pf.eof() )
{
p.clear();
if (p.parse( pf.getLine() ) == true )
{
printf(“Parsed:\n”);
for ( int i=0; i<p.num(); ++i ) printf(“Word[%d] = %s\n”, i,
p.word(i).c_str() );
}
}
return 0;
}
Note that this code does not do anything different from our original code listing. It has simply been restructured to be more componentized. The logic and functionality remain the same. The code to read a file into individual lines has been moved into the ParserFile class (shown at 2). This class does more error-checking for input, and has specific methods to read the file and return individual lines, but it is otherwise functionally equivalent to the previous example code. Likewise, the Parser class (shown at 3) still parses a given line, but is no longer reliant on any file input to do its work. It is now a simple parser class that takes in a string and breaks it down into words, using a developer-supplied delimiter, in place of our hard-coded colon of the first example.
Looking at the main program, you can see how much cleaner the interface is, and how much simpler it is to read. It should also be considerably easier to debug, because each piece of the code is in a separate component, meaning that when a problem is encountered, only that component needs to be checked out.
Restructuring
Restructuring (also known as refactoring) is the process of going back through code and eliminating redundancy and duplicated effort. For example, let’s consider the following snippet of code (not from our example, just a generalized piece of code):
int ret = get_a_line(); |
|
|
if ( ret == ERROR ) |
|
4 |
throw “Error in get_a_line!”; |
|
|
ret = get_words_from_line(); |
|
if ( ret |
== |
ERROR |
) |
|
throw |
“Error |
in |
get_words_from_line!”; |
|
ret = process_words(); |
||||
if ( ret == |
ERROR |
) |
||
throw |
“Error |
in |
process_words!”; |
452 Technique 71: Reducing the Complexity of Code
This code is prime territory for refactoring. Why? Because the code contains multiple redundant statements, namely the exception handling (throw lines,
such as the one shown at |
|
4) To do this, follow |
these steps. |
|
1. Examine the code for similar looking statements or processes.
In our case, the code that is similar is the check for the return code and the throwing of the exception string.
2. Extract the redundant code and factor it into a routine of its own.
In this case, we can factor the code into a single routine:
void CheckAndThrowError( int retCode, const char *name )
{
if ( retCode == ERROR ) throw name;
}
3. Replace the existing code with the calls into the refactored code.
CheckAndThrowError( get_a_line(), “get_a_line”);
CheckAndThrowError(get_words_from_line( ),
“get_words_from_line” ); CheckAndThrowError(process_words(),
“process_words” );
4. If necessary, after the code is refactored, reexamine it for other similarities.
In this example, we might consider logging the error within the CheckAndThrowError function. This isn’t really a refactoring case, but rather an observation of what might make the code more complete.
Specialization
Programmers have a habit of writing code that is generalized to the extreme. Why write a routine that can break down a string into four parts at particular
boundaries, when you can write a generalized routine that can handle any number of segments — of any length each? Sounds great in theory . . .
One sad lesson — normally learned when debugging programs — is that generalization is really a pain in the neck. It causes vastly more problems than it solves, and it never turns out that your code is general enough to handle every single case that comes its way. So you hack the code to make it work; it ends up littered with special cases.
Take a look at an example of generalization and how it can get you into trouble. Going back to our original code, assume that your input file has very long strings in it — not really a valid input file at all. Suppose it looked something like this:
This is a really long sentence that doesn’t happen to have a colon in it until it reaches the very end like this: do you think it will work?
If we run our first example program on this input file, it will crash, because we will overwrite the end of the word allocated space. This happens because we generalized the input to handle any sort of file, instead of making it specific to the kind of input we were expecting. We could easily change our code to handle a bigger string, but instead, we should follow the rules of specialization:
Make sure that input files are clearly marked as valid input to the program: In nearly all cases, your program-specific input should contain a version and type identifier. We haven’t added this to this simple example, but it would make sense to modify the ParserFile class to read in a beginning line containing version information.
If your input is fixed-length, check the length before you start loading the data: If you have an input file that is supposed to contain words of no more than 80 characters, then any time you have not encountered a delimiter within 80 characters, you should abort the input process and print out an error message for the user. If the word length is not fixed, then you should never use a fixed-length buffer to store it.
Specialization 453
We already fixed this one in the ParserFile class by using a string in place of the fixed size buffer.
Reject data in any format you do not understand: This precept is a little easier to understand with an example. Let’s suppose that you are reading in a date from the command line or in a graphical user interface. Dates have so many formats that it is almost not worth enumerating them all. However, if you are given a date that is given as 1/1/04, there are numerous ways to interpret it. For example, it could be in M/D/YY format, and be January 1, 2004. Alternatively, it could be in D/M/YY format — which would still
be January 1, 2004, but would change the interpretation. There is no reason to keep the ambiguity. Either force the user to enter in a single format, or use a control that specifies the format.
This one really has no issue in our ParserFile class, because we aren’t dealing with specific data sizes.
If you follow these guidelines, you will cut down on the number of bugs you receive — which makes it easier to debug problems that you do encounter in your application.