Skip to content

Instantly share code, notes, and snippets.

@nicl
Last active July 16, 2018 09:54
Show Gist options
  • Save nicl/57ee0b1cbc5350e073405a8361d79220 to your computer and use it in GitHub Desktop.
Save nicl/57ee0b1cbc5350e073405a8361d79220 to your computer and use it in GitHub Desktop.
Branching thoughts

If you were to draw your code flow as a graph, what would it look like?

It's a useful question I've found in discerning what a good and bad architecture looks like.

Consider, the following two cases:

1)  /\
   /  \
   |  |
   ...

2) \  /
    \/
    |
    |\  // switching on case
    |\
    ...

In the first, branching happens early on, and then code paths are separate. Note, separate does not mean they don't share any helper behaviour. But it does mean that those blocks of code eschew if/switch statements based on type.

The second is characterised as a single flow but with lots of conditional branching.

To make the two examples more concrete, think of a website that must handle authentication of two different classes of users: visitors, and administrators.

In example 1 we would detect the class of user as early as possible, perhaps even via different routes, and then use different functions to handle each. Of course, some of the auth logic may be the same - connecting to the database, etc - but only if it is genuinely the same for each.

Case 2, by contrast, would converge onto a single code path. To handle the different cases, functions contain plenty of if statements to handle the different classes of user:

if (isVisitor) ..
else if (isAdmin)

This is a relatively simple example, but in practice we often find more than two classes of user.

So, why is the second case nearly always a worse choice?

Firstly, because it complects concerns, which makes it harder to understand things. If I want to understand the logic for an admin user, I need to also wade through code for visitors as well.

Secondly, it results in brittle code. It is difficult to introduce new cases because I need to understand and modify existing code. New cases may not fit within the assumptions of existing code, which may force a larger-scale refactoring.

Often things start simple, and these problems are less apparent, but as requirements change cases tend to diverge and multiply and the problems increase.

Are there any cases where the first approach makes sense? Perhaps when we can abstract different cases behind a single, simple, interface. In this way, we can avoid the explicit branching of (1) and some of the associated brittleness. As logic is encapsulated elsewhere, typically a class, we also improve readability.

However, abstracting things behind a single, simple, interface is difficult and often risky; requirements change, good abstractions are difficult and they may not be simple. If you have many cases though, or are writing a library and needs things to be extensible, this may be the best approach. The canonical example here is UI components, which all receive data and need to be drawn onto the screen somehow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment