I’ve been writing software professionally for the last decade, with perhaps another decade before that in personal projects mixed with a few side-hustle gigs. It feels like I’ve been learning at an absolute breakneck speed for the whole time — this is a huge part of why I still love what I do. A few things have shaken out, which I find myself having to explain over and over again to people whom I mentor and work with. I figured, better to finally write ’em down!
Everything Is Garbage, But It’s Better Than It Has Ever Been
It is tempting, as you write code, to think about how awful the tools you have are (“a poor craftsman blames their tools”). The network is slow and unreliable, everything is constantly being hacked, the platform you use leaks memory like a sieve and there’s nothing you can do about it, services you depend on get sold off and shut down… Your users are garbage too, they use your product for everything except what you designed it to do, type gobbledygook into your forms, run it in places you specifically told them not to, and expect it to run, bug free, for generations. What took me forever to realize is that everything I write is garbage too. In a way, it has to be — if it’s garbage above, and garbage below, how could it be anything else?
There are two great things about garbage. The first thing is that it’s really easy to improve on. That’s what people have been busying themselves doing for the last 50 years. The garbage today is so insanely better than the garbage a decade (or two, or three…) ago it makes me weep with gratitude. The tooling, libraries, and patterns have improved to such an extent that what an individual can accomplish today is just amazing.
The second thing is that it’s really easy to throw out garbage. “Refactoring” is just throwing out old garbage and replacing it with new, better garbage. The best code is written by people who know it’s all garbage — because they build it in such a way that it is easy to navigate through, understand, and improve on as y0u go. The worst code is the code that is created by those who think they’re fixing everything, and creating a gorgeous temple in a dump. You’re not. You never are, and you never will.
Write Code Like You Write English
Perhaps the most important lesson I learned from my mentor (Hi, Brian Fox
!) was to write and think about code in a similar way as natural language. Fundamentally, in english and software there are two “kinds”: things and actions. In english: nouns and verbs. In software: state and functions. When you write english, you are leveraging a lifetime of experience of your reader. Language is all metaphor — words correspond to concepts that you map onto reality. Do the same thing in software: code correspond to concepts that you map onto your problem domain.
Writing and reading code is much easier when you choose words wisely. When you read the code later, you will forget everything but the name (that’s the point!), and you’ll re-invent the conceptual structure based on the names. If you choose one that corresponds poorly to the function of that element in the larger system, you will introduce errors when you write code against it, because you’re working off of a freshly-created model, and not the real code. When you discover errors like this, fix the side that’s actually broken. Is the way that you’re using your already-coded system incorrect, or are you using a broken system correctly? Don’t break a great way of describing the problem in order to work around a bug in the underlying system! Don’t be married to a way of describing the problem that has inherent flaws that you have to fix lower in the system! When building things I often go back and forth across the conceptual/execution boundary. In describing the problem, I define data structures and operations. Then I implement them, only to realize that I described the problem slightly wrong — then I have to re-define and describe the problem, and then re-implement them.
This sounds inefficient, why not just walk away as soon as it “works”? Because most of your time won’t be spent writing code, it will be spent understanding and fixing old code (because all code is garbage!). If there is a clear intention and implementation, it will be trivially easy to figure out what the bug is, because 95% of the time it will be as simple as “oh, the model is wrong” or “oh, the implementation ignores this now-obvious edge case”. You want to minimize situations where there isn’t clear intention and execution, because they’ll absolutely crush you down the road.
Curiosity is King (or Queen)
It is vitally important to understand your environment and problem domain as best as you can. This includes your language of choice, your libraries, your users. If you ever find yourself wondering about something — go figure it out. This has two benefits: it’s usually fun and it helps you write better code. If some piece of code is behaving in a way that “seems off” — it is vitally important that you resolve it. Either your understanding of the code is incorrect, or the code is incorrect: both are crucially important to resolve. If you’re wondering how a function is called, figure it out. Test your assumptions. Build up a good map of the code you’re working with.
Who Are You Coding For?
The naïve answer is: the computer, of course! However, the most important consumer of your code is you. If you’re lucky, it’s some brave soul using your code in the future. Keep this in mind always. What are the questions future you is going to be trying to answer when reading the code? Many modern coding trends run totally counter to this notion. DRY (don’t repeat yourself) is a terrible motto. Clarity, legibility, and understandability are infinitely more important than not typing the same thing a few times. What is the most clear, simple, way to express what you are trying to do? Usually (in defense of DRY), if you’re typing the same things over and over and over, it’s not clear or simple… but sometimes it is.
Using fancy language features to cut character count has a terrible endgame — jumping through 3 levels of abstraction to understand what’s going on is awful. Have some respect for future-you: write things in such a way that you will immediately understand what’s going on, how, and why. More of your time will be spent reading code (and comments) than writing it, so optimize for reading — not your typing speed. Some questions come up a lot, “where is this code called from?”, “what code does this code call?”, “what is responsible for this piece of UI?”, “where is that URL coming from”. When these questions can’t be immediately answered by reading the code or a full-text search, write the answers in a comment. A lot of the dynamic programming practices these days make those questions harder to answer. Dynamic function names, dynamic text, and modular UIs are all useful, but they come at a cost: you can’t just
your way to an instant answer. If that’s a compromise worth making, and it certainly is sometimes, leave a reasonable breadcrumb trail in the form of comments, documentation, and a predictable project structure.
Beware The Edge Case
Edge cases are conditions that happen “sometimes”. It’s easy to ignore them — how often is someone going to get your code to divide by zero? The answer is: all the time.
As you’re coding, it’s important to keep two things in mind. First: how is this code going to be misused (what are valid ranges of inputs, etc.). Second: how am I going to report errors? Making sure that the consumer of your code can deal with faults is a top-level feature. Don’t relegate it to exceptions or error codes. The network succeeds way more than it fails, though it’s easy to forget it. Integers are way more likely to be non-zero than zero, but afer a few years, it sure doesn’t seem like that.
The 10-Year Forecast
In conclusion, I want to reiterate that we’re never going to have a perfect system. Imagining some utopia where there is 10x productivity or zero bugs is likely to lead you to some counter-productive decisions. Pragmatic solutions to real problems, written in a way that is tolerant of errors, that is easy to read, navigate and undertsand is the best you can hope for. If we all keep building software this way, we will continue the amazing trend of the last 50 years. Try to make life a little better for future-you, and the rest of us will reap the benefits as well.