Life is really simple, but we insist on making it complicated.
About a year ago, my team and I were working on a feature that turned out to be complex and unpleasant to implement. Multiple system dependencies, complex user interactions, unreliable third-party endpoints with which to integrate – the works. As we often do at Wetpaint, we had a junior and a senior members of the team working together on different parts of the feature. We were going through a code review round. Our senior engineer was visibly uncomfortable with some of the code “smells” caused by the overall complexity and wanted to simplify things, even if it took extra time to do. Being a team joker, he said: “Imagine that the next maintainer of this code is a serial killer who knows where you live. You don’t want him to get angry, right?” We fell over laughing, continued to listen to our instincts and worked to considerably simplify our code.
Looking at this funny incident now, I appreciate even more how undeniably prudent and forward-looking this advice is. Simplicity is a virtue. We should try to maximize it.
When I say “simplicity”, I realize how relative this term is.
Nowadays, in addition to “serial killer” test, I apply one more: can somebody new to this code but not new to the problem pick up the gist of what you built quickly? Over lunch? Over a day? When you have moved on to other things, teams, companies and cities?
This requires, in my mind at least, that I and my team try to follow these simple guidelines:
- Be intentional. Naming things right is an art that must be mastered. Calling things for what they are has extraordinary importance. I make time to allow myself to think of a good name for that method or that class. Among other things, it reduces the need for comments I have to put in my code (more on that below).
Be brief but not obscure. If anything can be expressed in a few lines of code and still well-understood by a relative newbie, that’s a perfect balance. I don’t believe that stuffing 50 things into one line of amazing C++ is as useful.
Be fluent. I frequently consider returning self (or this) as method results, so that calls can be chained together into English-like sentences:
def service GoogleAnalytics. with_credentials(credentials). using_caching(interval). using_table(all_traffic) end
Delegate responsibility to an Information Expert. I follow this pattern as much as I can, while deferring decisions about which component will handle each part of the execution until last possible moment.
Refactor. More often than not, I realize that initial class and method structure won’t work as I get deeper into development. As soon as this is discovered, I believe that trying to force the issue will cause more discomfort and waste more time than doing a quick refactory. Refactoring shipped code is 10-100 times more expensive than doing it during feature development.
Avoid comments. I know this is controversial. But consider the fact that most of the comments we see in code are either obvious and redundant (once you actually read the code) or, much worse, are placed in blocks of code that are so complicated and unreadable, comments don’t add value anyway. Therefore, over the years, I’ve grown accustomed to a style of programming when the only comments I should need are the ones from which we generate documentation. Everything else, most of the time, is a sign that naming is screwed up or complexity is too high. So I either refactor or rename things to signal intent better that a comment could.
Don’t build frameworks right away. Until I see the same code pattern repeat itself in the project multiple times, the simplest approach is the best. Once I begin noticing repetitious code, I DRY up my code up paying attention to varying requirements and giving my DRY pieces just enough flexibility to handle the needs of the project. Thus, a framework emerges organically, out of real code needs.
Keep learning experiments out of production code. The fact that I just learnt how to use, say, Backbone.js does not neccessarily mean that my entire project needs to be re-built with it just because I want to learn how to apply my new shiny knowledge. Best tool for the job principle still applies.
Be test-driven and test-obsessed. I strive to write tests not just to verify behavior of my code, but to explain to others intricate details of how my code functions. To do that, I try to be thorough with test coverage, mock or stub liberally, be very intentional in how I name my tests, and very careful with what expectations I assert at the end. This also helps with reducing amount of comments and instead replacing them with working test code.
I am not naive to think that every problem can be solved with these simple recipes. Some problems are naturally very hard and require big chunks of very complex code. But I am advocating going the extra mile and doing everything possible to make things simpler for people who will work with our code with us and after us.
Because overly complex, unreasonably “cool”, or utterly unmaintainable code quickly becomes legacy code. Legacy code that nobody wants to touch. Legacy code that does magical things nobody understands. Legacy code that is dead weight capable of sinking any software product.