8 Habits for Developing Great Software
Ever get the feeling you’re writing legacy code?
I’m using legacy code here in the broad sense meaning anything that makes software hard to maintain and grow. Work on any project more complex than Hello World, and opportunities abound to create spontaneous balls of mud. The new year got me thinking about how we at Adapptor manage to avoid them.
TL;DR: The price of keeping your client’s software project alive? Eternal vigilance… with a human touch.
Let’s unpack that in the form of 8 habits.
1. If it smells, stop and deodorise. Now.
If your nose is twitching, take five to find the source of the smell. It could be a function that is difficult to name, a circular import, or logic that relies on an implicit assumption. If you find yourself writing comments that begin:
Stop. Bad developer. Don’t do it.
There are many and better articles on code smells — what they are, how to spot them, and how to fix them. My point here is to cultivate the habit of fixing them now, as it will save your client pain. Plus, procrastination is bad for your health.
2. Conventions, use them
Coding is a human activity — done by humans (at least for now) for humans. Far more time is spent reading code than writing it. So code for your audience.
Conventions facilitate shared understanding. Pretty much everyone on the planet knows what a stop sign means. It’s the same with code. If the codebase you’re working on prefixes network request functions with get, don’t prefix your new function with fetch. On seeing this, the first thought in the mind of the next developer will be, “Wait — does this do something new?” Minus one minute of checking to confirm that, nope, it’s the same as the others. These minutes add up, and are an annoying waste of time.
Conventions can apply to much more than naming, including project file structure, layout, patterns of resource disposal and error handling. Use editor extensions and linters to automate what you can. Check if your framework or libraries have best practices and conventions, and use them. It’ll be that much easier to solve problems when consulting online help. Be willing to adopt the conventions you find, and be consistent.
3. Anticipate change
But didn’t you just say — ? Using conventions doesn’t mean they’re set in stone. Assume you will need to update them. Why? Change is not an if, it’s a when, because your code sits on many layers — framework, libraries, language, OS, silicon — each of which is subject to continental drift.
Sometimes all that’s drifting is the zeitgeist (‘classes are so last year’), but even that might impact the desirability of your workplace to potential recruits, so can’t be ignored. If your code needs to be updated, look for ways to gradually opt-in to those changes. Simply updating a library that deprecates some functions alerts developers to the need to use the replacement functions. This makes a bridgehead and can help lower the barrier to change over time.
If your codebase isn’t mutable, it means one of two things: the project is dead; or your client is paying in blood for upkeep.
4. Don’t overfit
Avoid inadvertently writing a library in the course of your normal work. (Unless of course your normal work is library writing — in which case, go for it.) If you spot a pattern that can be consolidated — and devs are pattern-spotting machines — take a moment to consider the impact.
There’s an art to knowing where to draw the line. You might remove duplicate code at the cost of coupling what are actually distinct functions. I think of this in database terms as a kind of over-normalization: you compressed the database at the cost of more look-ups. In refactoring, those extra look-ups are with human 👀.
A good rule of thumb before refactoring is: how long will another dev (or your brain after Christmas) take to understand the layers you’ve introduced? If the answer is minutes, perhaps it’s not worth it. Sometimes code that is a little longer but simpler is better.
5. Be testy
Okay, okay. I said the T word. But hear me out. At least think about unit tests. Some folks get hung up on test coverage percentage; I couldn’t care less. Consider creating one happy-path test, particularly if your logic includes a non-trivial state machine. Even one test is a big gain. That test is broken ground, and it becomes that much easier for the next dev to add a problematic case when triaging a bug.
And remember, tests are also a kind of live documentation of your code — more shared understanding! A test called testDiscombobulateSprocket tells a dev that sprockets can be discombobulated. Who’d a thunk?
6. Eschew fossilized documentation
Speaking of documentation, if it’s not live it’s dead — and soon to be buried. By live documentation I mean it’s in your face or, if there’s a problem, will get in your face. In order of liveness, documentation includes:
- The code, using variable and method names to describe function, and ordered to tell a story
- Inline comments
- Summary comments
- Commit messages (Describe your changes, make them indexable! Avoid contextless tags like ‘Review actions’.)
- Readme and Changelog
- Adjunct HTML or Markdown
- Word documents and manuals created by your technical writers
- The deployment instructions on former employee Frank’s thumb-drive
A couple of tips:
Summary comments can be surfaced to others in your organization, e.g., testers, if you follow conventions and use a tool like Typedoc to automatically generate HTML or Markdown. This documentation is version-specific by virtue of being under source control.
Tie your Changelog to your CI build system. At Adapptor, we extract the app version from the Changelog file, and the need to update it is a constant reminder to keep the Changelog fresh.
7. Be civic-minded
I assume you’re using pull requests to control the flow of changes into your codebase. If you’re not, I suggest you seriously consider their benefits. In my experience, helpful colleagues can prevent a raft of bugs from reaching the more expensive (and embarrassing) stages of a project’s life cycle. Worst case, pull requests spread knowledge of your codebase across your dev team.
When you’re interacting with other developers over pull requests make the bar high to your not taking their suggestion. Remember: this is your audience speaking! And if you do decide not to act on a suggestion, provide a reason. This prevents you from simply moving on because you’re in a hurry or can’t be bothered. It indicates to the reviewer that you value their input and haven’t just ignored it. It can be annoying to add a commit to work you thought was done, but the benefits will accumulate.
8. Think like a CEO
This is a meta-tip that includes the above. Always be thinking about how your work helps or hinders the whole business. Does something save time, increase quality, encourage a colleague? Is your work adding value to a client, or did something get lost in translation? Is the software doing what you think it’s doing? Can you prove it?
Be responsible for your software after it has left the building. Consider adding analytics that help your project managers and clients see what it’s doing. You never know what opportunities that insight might generate.
And celebrate your wins 🎉. If you’ve just released a new version of an app, post it to Slack! or talk about it at coffee. Coding is a craft, and craftsmen thrive on a sense of accomplishment.
At Adapptor we develop and maintain software across a range of technologies from app to server-side, in various languages — TypeScript, Java, Kotlin, Swift, Objective-C, Go, Python — and at various stages in the product life cycle — greenfield to mature projects on SLA. It helps that our devs are on the same page when it comes to striving to be the best at what they do, while being kind and patient in the process.
If you’re aiming to improve this year, you could do a lot worse than to cultivate the above habits 😊