Hack, Refactor, Repeat
Tactics for being a more efficient and productive developer while still producing highly performant code.
I've been writing a lot of code lately. And I mean A LOT. Between AlertMe, some side projects, and an open source project I'm working on, I've been pumping out a ton of code. Luckily, it's mostly been using the same programming language (except AlertMe's Symfony app), so I've been able to really focus on producing some solid output. The problem comes down to my constant struggle between perfectionism and actually shipping something. In this post I'll discuss some strategies to ship code faster, without compromising quality.
I'm a recovering perfectionist. And every day is a struggle. It honestly doesn't matter what I'm working on; I just have this incessant need for it to be perfect. When I'd work on a frontend component, I used to complete the functionality of an item and then wrestle with the look of it for 20 minutes until I was completely satisfied. When I'd write database queries, I'd constantly look for optimizations and think about scaling it. When working on the backend, I used to rewrite a function five times until I was certain that it couldn't be done with any less code. You get the point.
I used to think that being a perfectionist was a good thing. Shipping bulletproof code just makes sense. But when you're trying to move fast in a startup and iterate on ideas, extra cycles spent optimizing code, refactoring functions, and making every little detail perfect, can be an epic waste of time. Even more so if the code you wrote never ends up in the final product. You obviously need to strike a balance between unstable and bulletproof code, so here are some thoughts on how to do that.
Pursue Simple, Get Fancy Later
I heard this quote while listening to a podcast and it has been one of the best strategies for fighting perfectionism. With complexity comes a higher likelihood of introducing bugs and creating edge cases. The simpler the component, the easier it is to write, test, and debug. Think about the smallest and simplest version of the feature or component you want to build. What are the absolute must-have features? What are the simplest forms of those must-have features? Build that and ship it. A handful of features that work are much better than a bucket full of buggy ones. If you have success with that, then start adding complexity.
Utilize Test Driven Development
Once you've decided on the functionality of your must-have features, start by writing a few tests. It's possible to be obsessive about tests as well, so just start with the basic, high-level tests. Writing a unit test for every function is overkill in the beginning. The goal is to write sturdy code that will perform well enough to test your new feature. A few simple tests that verify outputs is probably more than enough to make sure you don't break anything as you iterate. Once you're ready to move the feature into production, then beef up your tests to make maintenance easier and minimize technical debt.
Embrace Functional Programming
Regardless of the programming language you're using, you should strive to minimize side effects by writing pure functions. All languages behave differently, but writing 10 small functions that don't mutate data is much better that writing one big function. While it's tempting to package functionality together, writing small, easily reasoned functions will make code maintenance, testing, and debugging so much easier. Your collaborators and team members will thank you. Plus, you'll save time by not debugging a huge function line by line trying to figure out why your data is changing.
Leave Yourself Notes
Whenever asked by team members about our coding standards, I always respond, "Comment, comment, and then comment some more." I know there are people who argue that "comments become out-of-date too quickly" and that well-written code is self-documenting. I don't necessarily disagree, but it depends on what types of comments you're writing. I prefer to use comments as a way to explain my own thinking. Why did I do something this way? What were the other ways that I tried to solve this problem that didn't work? This helps both future developers looking at your code, and you when you go back and look at code you wrote some time ago. I also judiciously use FIXME, TODO, CHANGED, IDEA, HACK, NOTE and REVIEW in my code. This allows me to quickly locate these comments within my IDE. Leaving myself little notes lets me move on from something because I know I won't forget something.
Don't Worry About Scaling
I know the question you're likely to get from your product manager: "But will it scale?" Unless you are adding features to an app with tens of thousands of users, this question probably shouldn't concern you. If you're starting an app from scratch and you have zero users, this definitely doesn't concern you. If the purpose of the feature/component is to test an idea and then iterate on it, scaling honestly doesn't matter at this point. I heard the founders of Drip (a marketing automation company acquired by Leadpages in 2016) discussing how they are still using a single Postgres instance as their database. Also, do you think Facebook is still running code written by Mark Zuckerberg in his college dorm room? If you are lucky enough to have your code accessed by millions of people, then refactor for scale.
Using these strategies has helped me and the teams I work with to be more productive while still producing high-quality code. Once features/components are adopted, refactoring for optimization and additional stability is an absolute must. But premature optimization, a barrage of unit tests, and obsessive attention to every little detail will ultimately result in a lot of wasted time. Write good code and you'll move faster, learn more, and ultimately be a more efficient and productive developer.