My hands positioned artistically on the workbench in some sort of metaphor for quality code
Before we get overwhelmed, lets look at the basics.
What makes good procedural code?
At the function level
When dealing with inexperienced developers, I always encourage them to first focus on their code at the level of a single function. My go-to book recommendation for getting these basics down is Uncle Bob’s “Clean Code”
If you look at Chapter 3, “Functions” in this book, you’ll see the sorts of sensible recommendations you’d expect when learning to write well formed functions. They should:
- Be small
- Do one thing
- Have one level of abstraction per function
- Use descriptive names
- Not have too many arguments
Of all of those, the most important to teach new developers is often “Have one level of abstraction per function”. This takes experience to get right, but tends to drive many improvements to the code.
One level of abstraction
Here’s an example of a function that mixes levels of abstraction:
isValidOrder mixes levels of abstraction
We’d expect a function like this to check multiple things on the order to ensure that it’s valid. Each of these checks should be similar in that they likely require understanding of the order. Object-oriented developers will of course notice that in OO code, this would probably be a method on an Order class called “isValid”, but even if so, the same criticisms I’m about to make would apply.
I want to be able to read this function as a list of clearly understandable checks performed on the order. At the moment there are three checks, but I’d also be aware that more could be added later. The current three are:
- The order is has more than one unit
- The order is marked as complete
- The total cost of the order (including tax) does not exceed the customer’s available funds
These three checks as I’ve expressed them in words are on the same level of abstraction, but in code it’s a different story. The third check performs some mathematical operations to figure out the price including tax. When I’m reading a list of checks I don’t want to have to look at the multiplication of tax percentages and check that it is doing what I expect. Hide that code somewhere in a function that is all on that lower level and knows about tax percentages.
Even without using any Object-Oriented techniques at all (such as adding methods to order compute the with-tax amount, or to customer to determine if it can afford it) in the very least lets extract that math into a function.
The sales tax example is easy to spot, because numerical calculations are almost always a lower level of abstraction to other business rules. A similar rule of thumb could be applied to string operations, file management, HTTP calls, and so on.
Keeping functions at the right level of abstraction, and giving them a single and clear thing to do, also ties in to naming them well.
In their article “Name That Thing”, experienced developer Charlie Ablett writes:
Within a function, variables also need to be well named. Variables are often used to explain part of something more complex. Don’t worry about saving yourself typing time, while a long name is not always the best either, too long is definitely better than a one-letter abbreviation.
At the module level
If someone can write high quality functions again and again, then the next milestone on their journey to producing a high quality code-base is how they organise their functions. Even procedural code that does not contain object-oriented structures needs to organise related functions into concepts that can help the developer understand and decompose the overall problem.
Units of code which are not instantiate-able classes are typically called modules. Modules don’t connect data and behaviour the way that classes do, but they do encapsulate a set of related behaviours into a cohesive unit of code, and can even be used polymorphically (ie, you might chose to use one module or another, where both expose the same functions).
At the Directory Level
If a file is a module, and thus a unit of code, then our directory structure represents some sort of taxonomy of those files. The tree structure of directories is not an ideal taxonomy for all purposes, and shouldn’t be overused. Sometimes, with well named files it’s worth considering a fairly flat directory structure, particularly for things like React component libraries.
When we do use our directory structure to organise our modules, we only get to use it to break down our application according to a single set of criteria, because a file can only be in one directory. Here’s an idea to consider:
Organise your files based on your problem domain, not based on your architectural pattern.
If you find yourself splitting your code up into directories like “models, views, controllers”, then I think you’ll find that this will make it harder for new developers to understand the key parts of your application, and will also reinforce the tendency of your application to become a single monolithic code-base which you can’t later split up.
That’s all from me, catch you again for part 2.