Splitting Up a Large Procedure

Vinson Chuong
Scripting Bits
Published in
2 min readJan 7, 2020

While maintaining test coverage

I’ve found it a pretty common occurrence for codebases to have isolated, step by step procedures, whether its fetching and cleaning all of the data related to a resource from an external API or a shell script for deploying an application.

Because the steps of the procedure often cannot be reused outside the procedure, these procedures tend to grow, their test cases growing with them.

What starts as a 30 line function could grow to hundreds of lines.

A natural refactoring would be to extract distinct steps into separate functions, calling them from the main function.

The test cases for the main procedure would also be split up and moved into the test cases for each individual step.

When we look at each step in isolation, we have more flexibility to test it with different combinations of inputs. We’re not forced to also run each prior step and each subsequent step.

Because the main procedure still depends directly on each step, it needs a test to verify that it uses each step correctly — a spot check. There’s not really a way to escape duplicating some test coverage in this way. It doesn’t make sense to introduce indirection to invert some of the dependencies because the steps are probably not reusable.

So, as it stands, we took a large procedure and extracted sub-procedures, each directly depended on by the main procedure. The main procedure would look something like:

import one from './steps/one'
import two from './steps/two'
import three from './steps/three'
import four from './steps/four'
export default function(input) {
let operand = input
operand = one(operand)
operand = two(operand)
operand = three(operand)
operand = four(operand)
return operand
}

It occurred to me while I was performing a similar refactoring that, while we can describe the refactoring as extracting the steps into separate functions, we can also describe the refactoring as defining an interface for performing multi-step work.

Suppose that we changed the above code just a bit:

import * as steps from './steps'export default function(input) {
let operand = input
for (const step of Object.values(steps)) {
operand = step(operand)
}
}

It’s an interesting inversion of control — define a bunch of steps and this framework will run them in sequence. It enables adding and removing of steps without needing to change the above code.

Weird discovery…

--

--