Interface and Exception

Interfaces and exceptions have some synergy going on about them. Recently, I have been employing exceptions to preserve (meaningful) interfaces and redirect control back to the calling context. It is surprisingly elegant — at least compared to some alternatives. I've seen functions trying to return 2 different classes of information to its caller by using a 2-element array: the first acts as a status flag and the second either an object or a string depending on the status. This approach is as awkward to use as it is to describe. Here is a fictional function:

/**
 * @var $id
 * @return mixed[]
 */
function getQuery($id)
{
    $type = getType($id);

    if ($type === 1) {
        return [true, CachedQuery::createInstance($id)];
    }
    
    if ($type === 2) {
        return [true, NormalQuery::createInstance($id)];
    }
    
    return [false, "{$id} is not a valid query"];
}

This design is problematic. It does not have a reliable interface (nor can it support one) and it burdens the caller with status checks before it can safely use the main response data. Here is how a typical caller would look like:

$response = getQuery($id);
if (!$response[0]) {
    log($response[1]);

    return null;
}

$object = $response[1];

That's not very readable and looks really sloppy. It makes several assumptions on the response despite the lacking guarantees of the called function. Let's try again. Here is a better caller:

$response = getQuery($id);
if (!$response) {
    return null;
}

$statusOk = $response[0] ?? false;
if (!$statusOk) {
    log($response[1] ?? '');

    return null;
}

$object = $response[1] ?? null;
if (!$object) {
    return null;
}

This is much better in that it doubts the response of a function especially since no guarantees on the type of data returned is made (defensive programming anyone?). However, this is not a very efficient way to program either. Needing to doubt every variable is simply too time-consuming and tiring and it is bound to affect readability and program performance in more complex situations.

Proper interface design, type-hinting, and appropriate use of exceptions can eliminate this class of problems. Obviously, a function that returns an array with mixed elements makes use of none of these concepts hence the caller (if sensible) will have to compensate with an overly defensive design. Here is how to use an exception to achieve a reliable interface.

/**
 * @var $id
 * @return Query
 * @throws NotSupportedException
 */
function getQuery($id): Query
{
    $type = getType($id);

    if ($type === 1) {
        return CachedQuery::createInstance($id);
    }
    
    if ($type === 2) {
        return NormalQuery::createInstance($id);
    }
    
    throw new NotSupportedException("{$id} is not a valid query");
}

Aside from the argument which isn't the subject here, this function's interface is clear and reliable. It will only ever return a Query object or throw a NotSupportedException if no Query is available for the id. These things allow the caller to make safe assumptions and be more relaxed.

try {
    $query = getQuery($id);
} catch (NotSupportedException $nse) {
    log($nse->getMessage());

    return null;
}

Doesn't that look so much better?! 😍

Show Comments