Controlling the control flow

There are various means of managing the flow of control through the code. You can have an if statement, where the outcome of the condition evaluation determines whether to take the if or the else branch. Or you can have a while loop where the condition determines whether to run the loop again. The flow of control basically answers the question: which statement to execute next?

This post discusses various patterns that affect the flow of control.

Separating condition and evaluation

For an if statement, it is possible to separate the point where the decision is made from the branch point. Here’s what I mean:

bool handleSpecial = NeedsCarefulHandling(package);

...

if(handleSpecial) {
    package.AddHandleWithCareSticker();
    slowLane.Add(package);
} else {
    fastLane.Add(package);
}

This example introduces special handling for fragile packages: these packages go to the slow lane and get an additional warning sticker. However, the decision about how to handle the package has been made in a separate part of the code, before the if condition is evaluated.

I consider this bad practice, because it negatively affects the maintainability and readability of the code. When you need to understand why the if or the else branch was taken, then you need to understand how the condition was calculated. Therefore, having the if statement and the condition together helps in understanding the code. Separating them makes the code less readable.

The question then remains: how can we introduce special handling without applying a boolean flag? The answer lies in the Single Responsibility Principle: keep everything of a single responsibility in the same place in the code. In this case: the function that adds a package to a queue must also decide which specific queue to add to. This might even mean that the NeedsCarefulHandling contents may be inlined in the function where the if resides.

Managing control flow from outside

There are also smells/anti patterns that affect control flow. The boolean flag pattern is such a smell:

public void EnqueuePackage(Package package, bool isFragile) {
    if(isFragile) {
        package.AddHandleWithCareSticker();
        mSlowLane.Add(package);
    } else {
        mFastLane.Add(package);
    }
}

// calling code
EnqueuePackage(package, true);

Here, the caller of EnqueuePackage must determine how the package is actually handled. The caller determines which of the branches of the if is taken inside the EnqueuePackage method.

Passing a boolean to affect control flow is an example of the Flag pattern. How can we fix this? Since the caller already determines which branch of the if to take, we can just pull up the if construct to the caller. To do this, we need to split up the EnqueuePackage method into two. The result is as follows:

public void EnqueueSlowLanePackage(Package package) {
    package.AddHandleWithCareSticker();
    mSlowLane.Add(package);
}

public void EnqueueFastLanePackage(Package package) {
    mFastLane.Add(package);
}

// calling code
EnqueueSlowLanePackage(package);

In the above example, the condition has gone completely. That is because the calling site always wants to add to the slow lane. If this is the same for all call sites, then this change makes the code much simpler to understand.

Managing caller control flow

The opposite is also possible: then the called function determines what the client has to do:

public bool EnqueuePackage(Package package) {
    if(mQueue.IsFull) {
        return false;
    } else {
        mQueue.Append(package);
        return true;
    }
}

// calling code
bool success = EnqueuePackage(package);
if(!success) {
    HandleQueueFailed();
}

Here, the burden of the handling of queuing failures is put on the calling code. The client code therefore has to branch on the return type: if you forget, then packages might vanish without notice. So this construction is unsafe by design!

How can this situation be fixed? Again, according to the Single Responsibility Principle, one piece of code should handle a responsibility completely, including reasonably expected error handling. Instead of handing over the full queue problem to the consumer, EnqueuePackage could also block until place becomes free.

Exceptions also affect control flow

We haven’t discussed exceptions yet, but those are also a form of control flow. We can even consider them as goto in disguise: they make it possible to jump to any place on the stack.

You could use it to change the flow of control within a function:

try {
    for(int i = 0; ; i++) {
        array[i++] = 0;
    }
} catch(ArrayIndexOutOfBoundsException) { }

Or jump to a place in a calling function:

try {
    AnotherFunction();
    ...
    // to be skipped
    ...
} catch(Exception) {
    ...
}


public void AnotherFunction() {
    ...
    // complex control logic here
    ...
    if(...) {
        if(...) {
            while(...) {
                if(...) {
                    throw new Exception();
                }
            }
        }
    }
}

To be clear: I don’t consider it good practice to use exceptions to control regular program flow! This is making the program very quickly very complex. Just use the regular constructions that the language provides. Exceptions are meant for, well.. exceptions, not for regular control flow.

Converting exceptions

A very interesting situation that I came across a while ago:

public bool TryDoSomething() {
    try {
        DoSomething();
        return true;
    } catch {
        return false;
    }
}

public void DoSomething() {
    // method that raises an exception on failure
}

// Calling code:
if(!TryDoSomething()) {
   HandleError();
}

This is nothing but a conversion from exception to bool. It is really pointless because it basically throws away all useful information from the exception, like the exception message and stack trace. HandleError() is now unable to distinguish between, say, an IOException or something else.

It would have been much easier to just omit the conversion to bool and catch the exception in the appropriate place:

public void DoSomething() {
    // method that raises an exception on failure
}

// Calling code:
try {
    DoSomething();
} catch {
   HandleError();
}

Conclusion

We explored a number of ways to change the flow of control through an application. All the ways we saw affect the quality of the code. Inventing fancy constructions to manage the control flow is almost always a bad practice. It affects the readability of the code, it introduces needless layers or is just a waste of effort. Simplifying control flow improves readability.

Leave a Reply

Your email address will not be published. Required fields are marked *