I was recently tasked with the, um, task of implementing some rather complex logic in a web site. I got my head around how it all worked, then created a pretty flowchart (in an effort to perpetuate the ruse that I am a mature developer who doesn’t just jump straight into code-writing).
When all stakeholders were pleased, I reached into my Thomas the Tank Engine toolbox and pulled out old faithful: the if statement.
As I typed away — building up my leaning tower of if — a growing unease, um grew. I was taking something perfectly suited to documenting a set of decisions (the flowchart) and turning it into some rather clunky code (if this then this else if switch this then this else this).
Why such a rush to abandon the flowchart way of thinking?
What if, pause, I tried to represent the logic of the flowchart as an object? If the object was generic and simple enough it would be quite readable. If that worked, I’m sure parsing the thing wouldn’t be hard.
Surprise! I have done this and would very much like to share it with you. You don’t have to, but if you print it out and put it on your fridge, I’d love to see a photo of that. It’s totally up to you though.
Let’s run through an example: building this here graphic in code.
This supremely simple flowchart describes how to decide what to show at the top of some fictional web page.
To start with I had a bit of a think about the key building blocks in a flowchart and what they do, and came up with the following:
- Some steps ask a question (is the user signed in?)
- Some steps do something (show the support widget)
- Some steps do something then ask a question (request ad for kittens, was an ad available?)
- The lines are paths from one step to the next, based on the answer to a question.
OK none of that is going to be difficult, let’s get started.
I am pleased. Gleeful, almost.
I decide that when it comes time to parse this tree, I will look at each ‘step’, assess some ‘test’, and move on to the next step based on the result of the test.
But soon dark clouds pass over my gleeful mood, as it dawns on me: if I don’t get organised now about handling asynchronous requests, I’m going to be sad before the sun sets.
Having mulled it over for a while, I decided the best approach was to categorise each step into one of three, um, categories.
1. Synchronous steps
This will include steps such as “is the user signed in”. With these steps, I can get an answer immediately, in other words, synchronously.
2. Asynchronous steps
These steps need to do something asynchronously (attempt to fetch some data) before being able to answer a question (was there any data?).
I will need a way to define an asynchronous action, and then have a test that has access to the results of the action.
3. Terminus steps
In the actual flowchart, you will have noticed that the green or teal blocks were the end of the road, and of course each of these will do something.
Let’s take a gander at some of these steps in more detail.
The word ‘gander’ made me think of geese which I fear/hate, now the red mist has descended and I’m unable to concentrate.
OK I’m back.
Pro-tip: no matter what ails ya, a pygmy marmoset will make it better. They are surprisingly tasty.
The average computer won’t understand “Is the user signed in?”, so I’ll add a
test expression that asks the same question in computer words.
To keep things simple, I have decided that the test expression must be synchronous.
Notice how the potential results of the test match the options under the
if property (
The results of the test doesn’t need to be boolean, it can be anything. One example of anything is a string:
OK let’s look at a step a bit further down. This one is reached if the user is not signed in, has performed a search, and is now viewing the singles category.
Since I have decided that each step must answer a question, this step must do two things:
- Request ad for kittens
- Ask if an ad was available
When it comes time to process this data, I will execute the action, expecting it to return a promise. When the promise resolves, I will pass the result to the test.
(If you’re not familiar with promises, you can consider this roughly the same as: “I will call the action requesting data, passing a callback. When the callback is called with the data, I will then pass it on to the test.”)
test: ads => !!ads is a bit too shorthand for your delicate tastes, you could write it out as a function like so:
Moving on with our journey down the decision tree, we arrive at a ‘terminus step’. The one below is the step that is triggered when there was an ad for kittens available (huzzah!):
Just a title and an action. Oh did I mention that the titles are superfluous? Yeah, the titles are superfluous.
In this case the action doesn’t need to be a function that returns a promise — we’re all done with the decision tree now so I just fire this last action and walk away.
I do not look back.
The finished tree is a rather portly gentleman, but still plenty readable, right?
Imagine discovering this code — having never seen it before — and trying to work out what happens for non-logged in users that haven’t searched yet. It should be just as easy to read as a flowchart, and easier than the equivalent pile of if statements.
Alrighty, we have our flowchart defined in a fairly simple object (not small, but simple. Like a giant idiot.)
Let’s parse this bad boy
It has come time to parse the
decisionTree object and I am, quite rightly I insist, more excited than I have ever been in my life. Skydiving? Just a bunch of air. Birth of first child? Meh, heaps of people have done it. But parsing the decision tree, that’s where it’s at, baby.
The parser is pretty simple really. It starts at the top step, runs the test, works out which step is next. Then it does it again and again and again until it gets to a step with no test — which means the end of the road.
Yet still there’s more…
The syntax of selecting the next step
When calling the next step, the parsing function takes the result of the test and looks for a prop with that name, like
step.if[testResult]. If nothing matches, the parser looks for a
step.if.default property. If that doesn’t exist, then you’ll get an “unhandled scenario” warning.
If you’re feeling queasy about me using the reserved words
false as property names, relaaaaaax, it’s been valid since ES5. Here’s a support table (you have to expand the Object/array literal extensions row).
If you’re feeling queasy about me confusing the string
'true' with the boolean
true, relaaaaaax!! When you call
propName isn’t a string, then
toString() will be called on it. And I know that you know that
true.toString() === 'true'.
If you were queasy about that you’ll be super queasy about this:
That feels so wrong, but it’s totally legit.
Taking it further
I could make this an npm package, call it
flowr or something snazzy. But I don’t really like the “giving back” aspect of open source. The “take take take” seems like much better value to me.
If I were to package this up, here’s a few thing I would do:
A final return
If I was interested in knowing when the decision tree had been parsed, and perhaps even what the final result was, I could change my outer function to return a promise, and have the final step resolve the promise.
console.log in the
then is a trick I only just realised recently. It’s the same as
something.then(result => console.log(result)). And to some of you I’m sure not even worthy of the ‘trick’ label.
More rigid checking
Half the purpose of this is to create something that’s easier to change next year and the one after that. But that future person might not know that
test should be sync and
action must return a promise if there is a test.
I should add in some
console.warn('test must be be synchronous') and
console.warn('action must return a promise if the step contains a test property') and so on.
In my example, I don’t have any steps that are reused; there’s no recursion within the decision tree.
But if there was: because
decisionTree is just an object, it can be broken up into smaller objects. For example if I needed to refer to the category branch of the decision tree in multiple places, I could split it out like…
I’m not sure that it’s easier to unit test this method than it is having an if statement labyrinth (holy crap I just spelled labyrinth right first go!).
But it’s certainly easier to make sure you cover all the eventualities, because you can just move your eyes down the screen and count up all the terminus steps.
It doesn’t take much brain power to glance as the below screenshot and see the 10 scenarios I need to cover. I could do it with my eyes closed, using my downstairs brain.
Fun fact: you have two brains, and that is not a dirty joke. 50% of your dopamine and 90% of your serotonin are in your second brain, which is made up of half-a-billion neurons and is not in your head. (This one is true. The one about the movie Se7en was not true.)
I’ve folded up the 10 terminus steps so they stand out (if you’ve got a decent text editor you’ll see a summary of what’s folded).
Let me take a moment to align with reality.
Is all this fancy footwork any better than a bunch of
Just for you, I have made the if/switch equivalent:
That’s 62 lines (including going overboard with the comments), vs my brilliantly simple 130 lines.
I seem to have strayed from reality in my excitement. Maybe all of this was my subconscious not wanting to do any real work and so I convinced myself that this particular wheel needed reinventing. I do find the desire to delay certain unpleasantries a powerful force.
Story time: many moons ago, in an effort to defer updating react-router (to 1.0.0), I tidied up my desk, adjusted the paintings on my wall, and made a coffee table. Five react-router versions later, I’m running out of room for coffee tables.
Bringing it back in
This is quite the emotional rollercoaster, I have my gut saying this is a good idea, but the line count saying otherwise. How about I do a pros and cons list: doin’ it flowchart-style instead of the more traditional if/switch approach.
- It’s (arguably) more readable
- It enforces thinking about (and handling) all scenarios
- If multiple decision trees are used throughout a codebase and all developers are aware of it, it offers nice consistency
- By separating the definition of the flow from the execution logic, a third thing (understanding how one works with the other) has been introduced. This needs documentation/understanding.
- Similar to the above, if this is implemented by one dev on a team but no one else uses it, it becomes yet another over-engineered approach to handling a problem.
- For a simple tree, it’s more lines of code. Maybe even for a complex tree.
Comment away if there’s something I’m missing. Like I even need to say that.
OK I just took a break and made dinner and had a surprise nap then ate cold dinner and I have come to a conclusion.
I do think this works as an idea — despite seeming convoluted — because it offers rigidity.
Something I just realised (in a dream, via a talking pony called Bruce) is that the natural state of any codebase is complete chaos. And given time, all codebases will return to their natural state.
The problem with a nest of if statements is that it’s easy to add an extra bit in here, and a little negative condition there, and oh I’ll just stick in an early return here, and so on.
As it gets messier over time, not only is it more likely to harbour bugs, but it becomes less readable. This increases the time it takes to make any future changes.
And readability of code should count for something. If your code clearly communicates what it’s supposed to do, then it is more easily maintained. At the end of the day, the same information is there, whether it’s in the shape of a ‘decision tree’ or a bunch of nested if statements. But if one method better demonstrates your intent, it is the better form of communication.
For example, I wanted to communicate to my upstairs neighbour that he should play World of Warcraft less loudly. When I communicated this information via a typed letter in his letterbox, it had no effect. But when I wrote the exact same words in lipstick on his bathroom mirror? — boom, no more World of Warcraft.
My point, if there are two ways of writing some piece of code, I’ll prefer the one that more clearly communicates the intent of that code.
For those that asked, here’s the full thing as a fiddle.
If you think this is a good idea, click the little heart once. If you think it’s a shit idea, click the little heart three times. Every vote counts.
Did you notice, not a single mention of know-it-all.io in this whole post!