The flat success path

Filipe Ximenes
January 9, 2018
<blockquote>For Pythonistas this could also be called "why flat is better than nested"</blockquote><p>If you want to write clear and easy to understand software, make sure it has a single success path.</p><p>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.</p><p>The second thing it means is that the success path for a function should be clear in the flat most commands in it.</p><p>A note on flatness:</p><blockquote>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 <a href="https://en.wikipedia.org/wiki/Indentation_style">indentation</a>). <code>if/else</code> and <code>try/catch</code> 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.</blockquote><p>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 <code>divide(x,y)</code> function that receives inputs from the user, although the purpose of the code is to do <code>x / y</code>, you will need to check that <code>y</code> is not <code>0</code> before doing the calculation. Checking the inputs is fundamental for the correct functioning of the code but it's not the purpose of <code>divide</code>. 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.</p><p>Let's see this in practice, here is a function that transfers money from one user to another, returns <code>true</code> if it succeeds or return <code>false</code> if it does not.</p><pre><code>def transfer_money(from_user, to_user, amount): if amount &gt; 0: if from_user.balance &gt;= 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 </code></pre><p>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:</p><ol><li><code>if/else</code> clauses and nesting makes it hard to identify which is the main flow, the main thing this piece of code is trying to do.</li><li>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.</li></ol><p>Let's now refactor:</p><pre><code>def transfer_money(from_user, to_user, amount): if amount &lt;= 0: return False if from_user.balance &lt; 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 </code></pre><p>Notice that despite looking a lot clearer, the refactored code has the exact same <a href="https://en.wikipedia.org/wiki/Cyclomatic_complexity">cyclomatic complexity</a> 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.</p><p>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:</p><pre><code>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 </code></pre><p>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 <code>if/else</code>s with <a href="https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html">Guard Clauses</a> 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 <a href="https://www.vinta.com.br/blog/2016/metaprogramming-and-django-using-decorators/">other blog post</a>.</p><p>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.</p><p>Thanks to <a href="https://twitter.com/cuducos">Cuducos</a>, <a href="https://twitter.com/VictorCarrico">Carriço</a> and <a href="https://twitter.com/AndersonRe86">Anderson</a> for reviewing this post.</p>