Blog

Ideas and insights from our team

The flat success path


For Pythonistas this could also be called "why flat is better than nested"

If you want to write clear and easy to understand software, make sure it has a single success path.

A 'single success path' means a few things. First, it means that any given function/method/procedure should have a single clear purpose. One of the ways to identify if you are doing this correctly is by trying to name that code block. If you can't do it with a simple name, you've got a smell. Being able to easily assign a clear name to a function means that it also has a single clear purpose and functions with single clear purposes are easy to understand.

The second thing it means is that the success path for a function should be clear in the flat most commands in it.

A note on flatness. A nested command is a block of code that is under a clause that visually moves the start of the code away from the left margin of the text editor (given that you are using good practices of indentation). if/else and try/catch are examples of it. Flat code is the opposite of nested code, it's code that is near to the left margin of the editor.

Success path might mean different things in different parts of a codebase: sometimes it is the default behavior of a function, in others, it is the most likely thing to happen, or simply the path with no digression from the main purpose of the code. For instance, when you are writing a divide(x,y) function that receives inputs from the user, although the purpose of the code is to do x / y, you will need to check that y is not 0 before doing the calculation. Checking the inputs is fundamental for the correct functioning of the code but it's not the purpose of divide. By definition, you won't be able to have a single flat success path unless there's only one purpose for a function. One depend on the other.

Let's see this in practice, here is a function that transfers money from one user to another, returns true if it succeeds or return false if it does not.

def transfer_money(from_user, to_user, amount):
    if amount > 0:
        if from_user.balance >= amount:
            from_user.balance = from_user.balance - amount
            to_user.balance = to_user.balance + amount

            notify_success(from_user, amount)
            return True
        else:
            notify_insuficient_funds(from_user)
            return False
    else:
        return False

This is a mess. It is not possible to understand what this function does from a quick look at it. This happens because of a couple things:

  1. if/else clauses and nesting makes it hard to identify which is the main flow, the main thing this piece of code is trying to do.
  2. Unless you read everything and understand what the function does it's not possible to know what are the return values for a success or fail execution.

Let's now refactor:

def transfer_money(from_user, to_user, amount):
    if amount <= 0:
        return False

    if from_user.balance < amount:
        notify_insuficient_funds(from_user)
        return False

    from_user.balance = from_user.balance - amount
    to_user.balance = to_user.balance + amount
    notify_success(from_user, amount)
    return True

Notice that despite looking a lot clearer, the refactored code has the exact same cyclomatic complexity of the first. Also worth to mention that measuring cyclomatic complexity is a precise mathematical concept that may indicate your code needs refactoring, flatness on the other hand relates to the semantics of it and is therefore more of a subjective evaluation.

The main change between the first and second pieces of code we showed is that if you read it ignoring anything that is nested you will end up with the main flow of the program:

def transfer_money(from_user, to_user, amount):
    from_user.balance = from_user.balance - amount
    to_user.balance = to_user.balance + amount
    notify_success(from_user, amount)
    return True

This is the success path. When one picks a new code to read it's natural to first try to understand flat most parts of it and only then inspect nested things which are naturally expected to represent digressions from the main data flow (special cases or error handling flows). Replacing if/elses with Guard Clauses is in general one of the best ways to expose the success path. We show how we combine guard clauses and decorators for some interesting use cases in this other blog post.

Not being able to achieve that kind of flatness is a sign that your code is doing too much and may be a good idea to separate it into multiple functions.

Thanks to Cuducos, Carriço and Anderson for reviewing this post.

About Filipe Ximenes

Bike enthusiast, software developer and former director at Python Brasil Association. Likes open source and how people interact in open source communities.

Comments