One of the best ways of learning how to write code is by reading other people’s code. Imitating what other people do, especially if it’s done well, is a very solid way to gain proficiency in any skill rather quickly. This holds true for a variety of skills such as carpentry, cooking, and for artistic skills such as writing, painting and also programming. We build on what others have done before us. That way, we don’t have to make the same mistakes over and over again: we benefit from the historic wisdom of the masses as it were.
Unlike with other skills, writing code can be done in any number of ways. Not only are there countless of ways to solve any given problem, there are also numerous possible implementations of any of those solutions. This raises the question: “what separates a good implementation from a bad one?”
About eight years ago, I got my first job as a software engineer on a game development team. I was about as green as I could be, and between not being very strong mathematically and not having any prior education in development, I was set for a challenge, to say the least. Fortunately, the gaming industry was chock-full of concepts such as behavior trees and fancy algorithms, which all made very little sense to me at the time.
Being the fresh, adamant programmer, I looked up the Wikipedia page for behavior trees, and started hacking together an implementation. I honestly couldn’t tell what a behavior tree looked like for the life of me, and I wasn’t very proficient in the language either. A lot of hacking later, the result sort of resembled a behavior tree, but it performed very poorly and it upset quite a few people. Something along the lines of “does this guy even know what he’s doing?”. Well, not really. But it’s not like I could tell them.
Ultimately, we did need a framework for expressing behavior trees. One of my colleagues decided to pick up the slack. Within a few days he wrote a far better implementation that I could ever come up with. What struck me most about it was its simplicity. While my implementation was a royal mess, his consisted of only a handful of types, and none of its methods were particularly complex.
Even though behavior trees were a very abstract concept to me at the time, it was through my colleague’s implementation that I first started to truly understand them. It was apparent that he understood the concept very well, for otherwise he wouldn’t have been able to write it in such a simple, succinct manner.
What Went Wrong?
First off, I wasn’t the person who should’ve done the work on such a central component in all of our products. Fortunately my colleague picked up the slack and actually did it right, perhaps even encouraged by my miserable failure, but still.
The thing that was wrong with my implementation was that for such a quintessential piece of our software, it was way too convoluted for anyone to figure out quick enough. All of our developers either wrote tree branches or modified them, which means that they all had to get comfortable with the API and thoroughly understand it. And boy did I miss that one.
Secondly, its performance was dreadful. To say the least. To give you an idea, the implementation we eventually shipped was about 2000% faster than the gunk I managed to produce. One of the big reasons it performed that well was because there wasn’t really that much code to begin with. The saying “less code is better” may not always be true, but in this case, it certainly was.
When writing code, the fewer context switches and side steps we have to make, the better. Our train of thought is very precious, and when it gets derailed, it takes a little while for it to get back on track. The ability to quickly figure our how something works, and being able to get back to our actual work is invaluable. This is probably the most important miss of my implementation, and something my colleague did very well. Anyone could take a look at the source code, and be able to figure out what’s going on relatively quickly. As I’ve since learned, in code, that is invaluable.
Consider Your Fellow Developers
The opposite of simple code is convoluted code. Convoluted code is the stuff of nightmares. You have to read through it at least three times for it make sense, and even if you think you understand it, the best you can still do is make a shaky call about its correctness.
One of the reasons people got upset with my behavior tree implementation is because they had to review it. In retrospect, I should probably have saved them from the torment of wading through endless lines of poorly written code, but it did serve as something of a wake up call. It made me realize that although I was writing my code to eventually run on a CPU somewhere, I was primarily writing it for other humans.
When writing for a human audience, simplicity is bliss. While computers may be able to deal with complexity rather well, but humans don’t. Our brains are far better capable to deal with simple code constructs than with difficult ones. Because of this, the cost of complex code is relatively high. While it may run just fine on a CPU, the next time it has to be extended or modified, someone will have to wrap their heads around it. Complex code may still be fairly trivial for the original author to interpret — anyone other than that person will require a significant amount of time to understand it well enough. Simple code on the other hand should be easily understandable by any developer, regardless of who wrote it initially.
Because of that, in my current team, we have made it part of our code review process to review your code prior to requesting a review. Having a final look at your code and scouting for places where you can simplify even a single line can go a long way, and prevents needlessly complex code from ending up in the code base.
What that encounter all those years ago has taught me is that simplicity in code can cause a rippling effect like no other. Simplicity thrives in environments where developer interaction is essential.
I found our focus on simplicity in code — sometimes even painstakingly so — has allowed developers to join projects with relative ease, it has greatly improved our code review process, and in turn the stability of our entire stack. Was it a conscious effort to put so much emphasis on simplicity? I think it was something that grew over time. Once I made it a goal to keep my code as simple as possible, others gradually noticed and picked it up over time, until it became a part of our guidelines and standards.
One of the biggest things about this philosophy is that it scales incredibly well. Inevitably, any stack will grow to the point where, as a whole, it becomes complex. At that point, the ability to very clearly understand each and every moving part with relative ease pays off hugely. You’ll be far better able to extend, modify or even replace any of these parts, even if they were written many years ago.
As with anything, simplicity shouldn’t be taken to the extreme. We’ve had vehement discussions over pull requests that were absolutely fine, just because the reviewer was of the opinion that a specific language construct was obtuse, and should be replaced by a simple
for loop. Of course, semantic discussions are good, but they should be agreed upon and defined in coding guidelines, not be the reason to reject a pull request.
Start small. Make it a personal objective the next time you’re about to submit a pull request for review to see if there is anything you can simplify in your code. If anything, it’ll allow you to look at your work from a different angle.
For those interested, the “good” behavior tree framework in this article is TreeSharp. I didn’t write it, but it has taught me an invaluable lesson.