Sunday, March 23, 2014

How to untangle code: Limit functions to void or const

Over time, software can become messy. Systems that start with clean and readable code often degrade with hastily-made changes to code that is hard to understand. I untangle that code, restoring it to a state that is easy to understand.

One technique I use with object-oriented code is to limit member functions to 'void' or 'const'. That is, a function may change the state of an object or it may report a value contained in the object, but it cannot do both.

Dividing functions into two types - mutation functions and reporter functions - reduces the logic which modifies the object. Isolating the changes of state is a good way to simplify the code. Most difficulties with code occur with changes of state. (Functional programming avoids this problem by using immutable objects which can never change once they are initialized.)

Separating the logic to change an object from the logic to report on an object's state also frees the use of reporting functions. A combination function, one that reports a value and also changes its state can be called only when a change to the state is desired. But a 'const' function can be called at any time, and any number of times, on the object because it does not change the object's state. Thus you can refactor the code that calls 'const's functions and change the sequence of their calls (and the frequency of the calls) with confidence.

Here's a simple example. The 'combined' function:

double foo::update_totals ( double new_value )
{
    my_total += new_value;
    return my_total;
}

can be separated into two:

void foo::update_totals ( double new_value )
{
    my_total += new_value;
}

double foo::total ( void ) const
{
    return my_total;
}

These two functions perform the same operations as the single combined function, but now you are free to call the second function (total()) as many times as you like. Notice that  total() is marked as const. It cannot change the state of the object.

Your original calling code also changes:

{
    foo my_foo;

    // some code

    double total = my_foo.update_totals( 100.0 );
}

becomes:

{
    foo my_foo;

    // some code

    my_foo.update_totals( 100.0 );
    double total = my_foo.total();
}

An additional benefit of separating mutation logic from reporting logic is that functions are smaller. Smaller functions are easier to comprehend. (Yes, the calling function is slightly longer, but the reduction "gains" in the class outweigh the increase in the calling classes.)

Messy code is ... messy. You can make it less messy by separating mutation functions from reporting functions, and ensuring that all functions are either one or the other.

No comments: