I believe a surer route to good programming is focusing on avoiding mistakes rather than focusing on doing things 'right'. In this episode, I give four tips for adding a modicum of rigor to your programming. First, proof-read all changes before committing. Second, execute every line of code. Third, double-check you're in the right file. Fourth, double-check your docs are for the right version.
July 26, 2020
No notes available for this episode.
Transcribed by Rugo Obi
This episode is about applying rigor to your programming.
Imagine you had a checklist of things to consider in order to guarantee 50% less bugs. What would that contain?
Programming is fast, whereas fixing bugs is time-consuming, especially when you have real users, real data, and financial consequences to things going wrong.
Adding a modicum of rigor to my programming is - without a doubt - one of the biggest force multipliers there is. All of this comes down to a fundamental realization I had about programming: It is better to strive to be NOT WRONG than it is to strive to be right. ie, it is better to focus on avoiding mistakes.
Tip 1: Proof Read Your Changes Before Staging and Committing Them
I'll start with an egregious example of what can go wrong.
A junior programmer I know was working on some code for login.
Here, you can see a line he added for debugging.
logger.info("\n Password is: #{user_params [:password] }”)
This adds a statement to the info level of the logger. Let's see what this does. I'm gonna log in here with my email and a fake password. That's "corporatemasterpassword", and I've logged in.
Let's have a look now and see what's going on with the logs. And you can see here, Password is: corporate_master_password
. We definitely don't want that to be logged in plaintext. If this made it into production - and it did in the case of the junior programmer I know - even though it wasn't a large codebase it still had consequences. All the passwords were logged in plain text, and they were available to the vendor of the server and also to the log services they used.
So, how did he end up adding this to production?
What I believe happened was he did a git add .
Once everything was working, and then just committed and posted without thinking too much. But if you take a look at what was actually staged there, by doing git diff --cached
, you can see these illegitimate changes added, this kind of logging statement, as well as the legitimate changes. (This was changing a translation string into a regular string.)
So I'm going to reset everything here and then stage it the proper way using git add -p
. Here I see each possible hunk, and I can choose whether or not to add it, carefully proofreading it at this stage. So this is illegitimate, so I'm going to choose n
for "no".These add the legitimate changes so I'm going to choose y
for "yes". Now, I just commit and it's going to be much better.
git add.
is a bug magnet. It’s much better to read over your changes piece by piece, whether that's with git add -p
or some sort of git UI tool, or something built into your editor.
What I just demonstrated was a particularly bad case of failing to check your changes before staging, or committing them. But truth be told, I noticed lots of noteworthy issues nearly every day when proofreading my commits.
Let me demonstrate just a few examples within Fugitive Vim, the way I generally like to do this.
So I have three sets of changes that I'm thinking about adding to a commit. So I press =
here and I open this up and you can see that there's a PHP file with a route. But you can see at the end of this line there is no semicolon, that's going to break the file and cause lots of issues. By checking these changes here, I catch this error early.
Then I'm going to check the changes for this eRuby file. And here I've accidentally lopped off half the file, perhaps with a Vim macro gone wrong or a cat on a keyboard. Checking my commits also allows me to check for these really dumb kinds of errors.
Next, we're going to check the changes for this typomethod.js
file, and that's a hint right there. So, if I’m proofreading this, I check that all the parameters and functions referred to, actually exist. So here we have this.makeUserAccount
. Unfortunately, this does not exist within this file. I have this.createUserAccount
. It looks like I changed my mind about what to call the method but I didn’t update it everywhere. So by proofreading here, I catch these errors early.
Tip 2: Execute Every Line Of Code Before Staging And Committing It.
In the previous tip, I nearly committed a line of code that had a function name that no longer exists due to renaming. If I’d actually executed that code before staging it, I would have known that this was an error. I would have gotten something like Uncaught TypeError: this.makeUserAccounts is not a function
.
Knowing about a bug now when it's just on my machine, is so much easier and faster to deal with and less embarrassing than when it has made it out into the wild or on some other developers’ machines.
Executing code is the ultimate way to inject a bit of reality into your assumptions. The interpreter is unforgiving - in a good way.
Now, most programmers execute their code from time to time. Where rigor enters in, is promising yourself you'll run it, no matter what, whenever you make a change.
I've observed the times when I failed to actually do this and come up in three categories of situations.
The first is when I've tested the code previously, and I'm now adding "one little change". Perhaps I've tested it three or four times before and I'm very confident it works and I just changed one variable name or something like that, then I assume it works. And of course, it doesn't work. Maybe that variable isn’t present in the function contet.
The second category of times when I don't execute the code again and therefore bugs enter, is when the code path is hard to reach, whatever that may mean, and therefore I just blindly trust it works. This gels well with the idea that the most common serious bugs in codebases are those in exception handling code. That's because exceptions are hard to reach, they're hard to trigger, and therefore this code doesn’t often get tested, and as a result, can have very serious bugs.
Hard to reach could mean for example, that you have to fill out a three-step login form and then wait for some async job to complete before you actually execute the code. Of course, there are ways around this, like, more modular design and unit tests and so on. And that's why these things are important.
The third category of times when I fail to execute code is when I've tested it and it works in some original context, maybe another program, maybe another namespace, whatever. Then I transport it to a new context and just assume it works there. Of course, often it doesn't. Maybe some sort of dependency is not present inside that scope.
The trick to adding rigor is to promise yourself to always execute the code again, no matter what. Just don't trust you have it right until you see it is executed correctly. If that means you have to change the design of your code to make it easier to reach or whatever, then so be it. And sometimes - and this bit can trip me up - you shouldn't just test that the code works but rather that the real world feature works, or that the intended side effect is produced.
For example, when I was recording this screencast, I wanted to change an internally used password, using the PHP Artisan Tinker console, which you're looking at here. So the first thing I did, is I instantiated the user that I wanted. Let's say it's the first one here for argument's sake.
php
<?php
$user = User::first()
Then, I set the password to, let's just say the letter s
here and saved it. Then I told my colleague "hey, go log in".
Colleague got back to me and said, login’s not working. So it's clear that I must have made a mistake here. What was my error? Well, I’d assumed that the password would be hashed automatically by the password setter on the user. Whenever my colleague attempted to log in, the framework took the hash of the plaintext password - i.e. it took that hashed string and compared it to what's in the database. Since I had only saved the plain text "s" in the database, the comparison was false, and therefore login was not possible.
What I should have done instead was set the password to the hash of this plaintext password. Once I'd done that, my colleague was able to log in.
The bigger lesson here isn't the specifics of hashing and passwords and so on, rather it's that my job wasn't done until I attempted to log in with a new password. It’s only through this action that I would have been able to flush out all my assumptions and actually check that my code had done what I had intended it to do.
Tip 3 In Adding Rigor: Are You Absolutely Certain That You're In The Right File?
In large projects, there are often many similarly named files.
For example, the same file name in a different context, a different namespace, or template files in a library that generates files with the same name in your repo.
For example, inside my Rails app, there is a webpacker.yml
file that I wanted to modify, in order to get hot-reloading to work.
When I modified the file, nothing changed and I was really pulling my hair out, and couldn't figure out what was wrong, so I went on a long and fruitless debugging session.
Now, if you look here at all the webpacker.yml
files in my codebase, you can see that there's the one inside my actual rails project, this one at the bottom. And then there's one inside gems/webpacker-4.0.7/.../webpacker.yml
, then is one from some other version of it (gems/webpacker-5.0.1
), and then there's one in node_modules
....
And if you look at the preview pane on the right-hand side, you can see that they're all pretty much the same. This is the kind of thing that could confuse you if you use some sort of editor shortcut to quickly jump between files and then you don't pay enough attention to what file you’re currently in.
Tip 4: Those Docs You’re Reading. Are They The Right Version Number?
Case in point, I was testing out the Tailwind CSS framework, and I wanted to get some text over here to be purple. In the video I looked at online - and in the docs I refer to (the official docs) - it said you used this class text-purple
.
Yet, when I added text-purple
here, the text did not turn purple. I spent ages trying to get this to work, I thought it was an issue with Webpack blah blah blah.
I tried configuring Tailwind differently, no success.
Do you know what the problem was? I was on the wrong version number.
I didn't know what the current version of Tailwind was, I just assumed the first results I found in Google were correct. In fact, the current version is 1.4.6, and it's text-purple-X
with a number afterwards e.g. text-purple-500
Let's try that out here. And it works.
This is something to look out for whenever you're googling for things online.
For example, you might be watching a video and it showcases code that is out of date, or you might be looking at a Stack Overflow answer, but it's from 2004. You should probably be sorting them by date in order to get the most recent stuff first.
That's all I've got to say for today.
Thanks for watching and see you next week.