Software Development Guidelines: Quo Vadis?

The following is a set of guidelines developed over my career as a software development manager at Apple and beyond, working on some of the most influential products ever created. I am sharing them in the hope that they are helpful to you. They are typically used to manage the day-to-day activities of a team of developers; writing features, fixing bugs, and generally managing the quality of their work, but some aspects may be applicable to other disciplines.

Einstein is not impressed
Einstein is not impressed that you decided to rewrite E=mc².

Goals

  • Respect & Consideration
  • Consistency
  • Readability
  • Usability
  • Maintainability

In Short

  • You are a member of a team
  • Code should be small
  • Conventions must be applied consistently
  • Conventions must be maintained
  • Don’t rewrite code
  • Don’t program rudely
  • Code doesn’t smell
  • Always document your code
  • You may be your own successor
  • Remain professional
  • Don’t duplicate code
  • Code must ‘just work’
  • Commit frequently
  • Always test your code
  • Code-review your own code (in addition to others)
  • Good software means good service
  • Everyone is your customer
  • Everyone files the bug
  • Spaces, not tabs 😂

Explained

You are a member of a team, and these guidelines ensure that your work integrates smoothly with that team. Similarly, they help make sure the efforts of other programmers do not unnecessarily hinder you from accomplishing your goals. They’re not rules, and they’re not perfect, but serve as the common understanding of the team. If you choose to deviate from them, pay careful consideration to the impact the deviation may have on your peers. As always, if you’re not sure, come talk to me. The objective of these guidelines is never to make your life more difficult than it needs to be.

Code should be small. ‘Small’ does not mean code should be ‘dense,’ or ‘convey as much functionality in as few lines as possible.’ Instead, it means code should be organized into functional components that are as small and lightweight as possible. This is beneficial because it makes it easier to modify or replace code later. Another way to express this is that the cognitive load of larger functions is higher. That is, to maintain a perfect understanding of what the program is doing at a particular point, the programmer must keep track of a lot of state in their head. Conversely, the cognitive load of ‘small’ code is lower. The programmer doesn’t have to think about too much to understand what ‘small’ code does. (See also cyclomatic complexity.)

Conventions must be applied consistently. To maximize readability, code conventions should be applied consistently throughout the codebase. That way, a programmer looking at the code of one particular module will not have to relearn how to read the code of a different module. (See ‘don’t program rudely.’)

Conventions must be maintained. It is common for projects to which many programmers contribute to ‘drift’ from their initial conventions. This is a self-perpetuating problem. When a new programmer comes upon a staid base of code, if he sees conventions applied inconsistently, he, first, will not know which conventions to apply, and second, will conclude that consistency is not important to the team. People tend to keep clean things clean, and let dirty things get even dirtier. Resist entropy.

Resist entropy

Don’t rewrite code. If physicists were software developers, we wouldn’t have E=mc². It would’ve been rewritten a thousand times by now. In fact, E=mc² probably wouldn’t even hold the same value for society, because someone would’ve come along, misunderstood it, and rewritten it using the fad of the day. If it ain’t broke, don’t fix it.

Don’t program rudely. If you are writing code that is hard to read, or forces those who come after you to re-learn new concepts or conventions with negligible benefit, then you’re being a jerk. Don’t be a jerk. The team is working hard to write code that is easy for you to read and doesn’t force you to relearn new concepts with dubious benefit, so don’t do it to them. (The golden rule.) And to be clear, we want new concepts if they are constructive and beneficial, but we also want to carefully choose the time of their adoption. Committing code that employs a new concept because you read about it on HN last night isn’t the right way to go about it.

Code doesn’t smell, and there’s no such thing as syntactic sugar. Be careful about the language you use to describe other people’s work. Words like smell and sugar have connotations that go far beyond the merits of any particular method of coding. When you use such language you alienate yourself and anyone who might take offense at your choice of words. This isn’t to say you shouldn’t advocate for or against things that affect your code or the code of others. Absolutely, you should. However, let the merits stand on their own. Be clear about implications by specifically describing their impact (flexibility, comprehensibility, clarity, expressiveness, etc). Do not use flippant language as a crutch for your argument. Also, never forget that if you don’t understand why something is done a certain way, there may be any number of reasons for that to be the case. Speak plainly and remain professional.

Always document your code. This means that every function should be documented and that comments should be dispersed throughout code to convey the intent of the code. The purpose of the comments isn’t necessarily to explain what the code is doing. Instead, it’s to convey your intent as the programmer. It may help your peers (or future you) figure out how a particular bug was caused, then fix it. If you don’t do this, a valuable source of understanding (your intent) has been lost.

You may be your own successor. i.e. you may have to maintain your own code. Many of these guidelines are intended to make easier the lives of programmers who come after you. Well, you may be your own succcessor, so be sure to follow these guidelines. You don’t want to get upset at some bit of code, only to find out the original author was you.

Remain professional. Everything you write can end up on the front page of the New York Times. Do not use foul or offensive language in your code, no matter how unlikely it is that the outside world will ever see it.

Don’t duplicate code. Perhaps this is another way of saying ‘don’t be lazy.’ We all sometimes fall into a trap of thinking ‘I know I should abstract this functionality out into a separate function, but I could get this done a lot more quickly if I just copy-and-paste this to a new function.’ Well, you’re right, but you’ve also just reduced the maintainability of the code base. In essence, you’ve sacrificed the long term for the short term. In an egregious case, duplicated code may contain a bug. You will have duplicated that bug. Then, later, when the bug is discovered, someone will go in and fix it in one place, not realizing the code was copy-and-pasted elsewhere. Then, the bug comes up again later in the pasted code, and everyone wonders why the dev team can’t fix a simple bug. Please, don’t duplicate code.

Craftsmanship is taking pride in your work.
Craftsmanship is taking pride in your work.

Code must ‘just work.’ This guideline means that, whenever you commit (or otherwise deliver your code to someone else), it must ‘just work.’ The person receiving your code shouldn’t have to resolve a bunch of dependencies, reconfigure everything, or patch the code to make it work. It should ‘just work.’ This is important because making the assumption that code should’ve ‘just worked’ when receiving it helps isolate problems in the inevitable situation that the code does NOT work upon delivery. That is, if we can safely assume the code was supposed to ‘just work,’ then we know that some problem occurred during the delivery. It could be that a file was not checked in, or that there’s a compatibility problem in a new environment, or something like that. If the code cannot be assumed to ‘just work,’ then we don’t know whether the problem was caused by a mishap in delivery, or the code contains some weird bug, or the code is experimental, incomplete, or whatever. In the worst case scenario, someone may spend a bunch of time trying to get a particular piece of code to work, only to find out that the code was never intended to work in the first place, and they were just wasting their time. Bad, indeed. If you must commit code that breaks a codebase, create a branch and commit those changes to the branch. Once your work is complete and you have confirmed it ‘just works,’ you may merge those changes into trunk.

Commit frequently. Don’t withhold changes for long periods of time. The longer you withhold changes, the more likely it is that your changes will conflict with someone else’s. In typical scenarios, you should commit several times per day, and not withhold changes overnight.

Always test your code. Code doesn’t exist for its own purpose. Code exists to accomplish some task. How, then, can you be certain that your code accomplishes its intended task if you don’t actually try it? What’s more, nobody knows more about the code you write than you do (before you deliver it). You have a better understanding of its potential shortcomings and flaws than anyone. (Before you check it in. After you check it in, all bets are off. Someone may study your code in detail. Think Heartbleed.) Therefore, test it. If you find a problem, it will be much easier to fix now than for some other programmer to later relearn what you were doing, then fix the bug that you created. It is true that the earlier a bug is found, the cheaper it is to fix. Don’t wait for QA to find your bug. Find it yourself, then fix it.

Review your own code (in addition to, and before asking anyone else to code-review it). This is a difficult skill to master, but is both possible and extremely valuable. The key is to focus on the process of the code review, and not attempting to understand the functionality of the code which you are reviewing (as you would if you were reviewing someone else’s code for the first time). If you focus on understanding the functionality of the code you are reviewing, you will waste your time. After all, you just spent a bunch of time writing the code, so what good will it do to try to re-understand it? What’s more, the fact that you believe the code is ready for review suggests you have a preconceived notion that the functionality is complete. It would be impossible for you to find flaws by focusing on understanding its functionality. Instead, focus on the procedure of the review. E.g. Review all modified files. Review all modified lines within each file. Review some of the context of the modified lines (especially call-stack context). Instead of reading the code and explaining to yourself what it’s doing, challenge yourself to answer the question “What is wrong with what I’m looking at?” How are these variables named? Are they confusing? Which data types are used and are those the right ones? For loops, are then any out-of-bounds conditions? Are there situations where the for loop runs more than necessary? Are all error conditions checked? Are the filenames correct? Is the code in the right place? Should it go somewhere else? What if its compiled for a different architecture?

Einstein wearing fuzzy slippers.
Einstein wearing fuzzy slippers.

Always provide as much service as you reasonably can to callers of your code. Good software is no different than good service. Be considerate. Obligating a programmer who uses your code to manage a bunch of state before your function may be called is inconsiderate. You know what state needs to be managed. Provide a simple initialization API for them to use and you manage that state.

Everyone is your customer. Your customer isn’t just a mythical end-user you’ll never meet. The people to whom you provide everything you create (code, binaries, documentation, status reports, bug reports, fixes, etc) are your customers. Manage the expectations of your customers (your colleagues, peers, me) just as you would as if they were directly paying you for your work. Make sure they understand what’s going on. Make sure they know when to expect something. When you suddenly learn you might not be able to meet those expectations, let them know, so they may make adjustments to accommodate that risk.

Everyone files the bug. When you hear of a problem occurring, recording a bug report yourself is the only way to make sure that issue is tracked. If you tell someone (a colleague, customer, whoever) to file a bug based on something they’ve said, and that person does not file a bug, then the bug never gets filed and falls through the cracks. If you both file a bug, effectively duplicating it, then the screener gets to pick the best one and marks the other as a dupe. Marking a duplicate is easy; duplicates are not a thing to be avoided. Also, having the duplicate in your queue means you gain the opportunity to verify the fix whenever the original bug is ultimately resolved.

Spaces, not tabs. 😂

Bob Burrough
February 8, 2019