27 May 2005

Exception handling in a limited domain

Two of the best features of C++ exception handling are transparency and the propagation of rich content. With good design, exceptions can eliminate intrusive error checking and the related structural code required to support that checking, making your error handling effectively transparent. Along with that structural code, error information based on POD types is replaced with class hierarchies that can provide a richer set of information on errors. I'm still refactoring code at work and have just ran across a good example of how exception handling simplified an area I was working on.

The refactoring is needed in order to isolate specific database access. The code needs to be migrated to a new schema, and it accesses several updated tables in multiple locations. Before I can add support for the new schema, I need to first isolate access to the altered tables. The original code cascaded database calls in order to extract a value from the database. MFC CRecordset [MSDN] classes are used for queries and enumeration. Each successive call acquires index values that are required to query the subsequent call. Mapping occurs similar to the following:

InputValue -> Table1.Field1

Table1.Field1 -> Table2.Field1
Table1.Field2 -> Table2.Field2

Table2.Field2 -> Table3.Field4
Table2.Field3 -> Table3.Field7

etc.

We can't use JOINs or sub-SELECTs because certain other field values are extracted along the way (Table1.Field5, Table2.Field8, etc.), and so we must walk through the process.

The original code was built around a combination of error returns and exceptions. CRecordset::Open() returns a Boolean for success/failure. After calling Open(), CRecordset::IsBOF() returns a Boolean for non-empty/empty. Open() (along with many other CRecordset calls) can also throw two exceptions: CDBException and CMemoryException [MSDN]. This combination of error values and exceptions resulted in code that was indented successively deeper as the cascaded queries were made, and that was ultimately wrapped by a single try/catch.

try
{
    // create query
    if (!open)
    {
        // report error
    }
    else
    {
        // process recordset
        // create new query
        if (!open)
        {
            // report error
        }
        else
        {
            // process recordset
            // ...
        }
    }
}
catch (const CDBException * e)
{
    e->Delete();
}
catch (const CMemoryException * e)
{
    e->Delete();
}

The first step I took was to isolate and abstract the database queries into a single function. With this single point of access, I could normalize the Boolean return value along with the exceptions into a single point of failure. I began with just returning a Boolean with the intention of adding the custom exceptions later. I then isolated the recordset iteration, which can also throw, in the same manner. Both the open and the iteration were implemented in a single function that takes a predicate and executes it within the required MFC try/catch block. With this design, I could remove the MFC exception handling and begin to simplify the points of possible failure.

// Open the recordset.
if (ProcessRecordset(rs, OpenPredicate))
{
    // Then iterate and process the records.
    if (ProcessRecordset(rs, MyIteratorPredicate))
    {
        // More work...
    }
}

The next step was to create custom exceptions that could encapsulate the relevant information from either failed opens or database exceptions. With these new exceptions that cover all possible points of failure, the conditionals are replaced by a single try/catch block.

try
{
    // Open the recordset.
    ProcessRecordset(rs, OpenPredicate);

    // Then iterate and process the records.
    ProcessRecordset(rs, MyIteratorPredicate);

    // More work...
}
catch (const CustomExceptionBase& e)
{
}

Is the added function and structure worth the resulting simplicity at the point of execution? The original inline code was reduced in size by 254 lines; the new classes and functions were 418 lines. It's a net increase although, as with any library, we gained reusability. The new code includes a smart pointer for MFC exceptions, templated access to parallel tables (similar to the problem I encountered earlier), and the streamlined recordset access described above. Any of these could be reused for simplicity and clarity elsewhere in the code.

[ posted by sstrader on 27 May 2005 at 11:14:33 AM in Programming ]