Ever since I studied Total Quality Management in college, I’ve been a bit obsessed with documenting processes. I was particularly influenced by Dr. Atul Gawande’s “Checklist Manifesto”, which underlined for me the value of collecting, documenting, analyzing, and continually re-optimizing best practices.
I wrote a while ago a 3-part series on communications:
- How to Write a Memo That People Will Actually Read
- How to Present so People Will Hear
- How To Add Powerful (and Legal) Images To Your Presentations
As part 4 in that series, I create a quick checklist for writing good code. Even if you’re not a developer, algorithmic thinking will help you to be more effective, so I think you can still use this list to guide your thinking.
There is a fair amount of worthwhile prior art on this topic. See these code review checklists and a Test-Driven Development Checklist, although also see David Heinemeier Hansson on Test-Driven Deployment is Dead, and the related Hacker News discussion . You may also find helpful this UX Project Checklist, and some tools to help in the process of crafting code cleanly and maybe slowly: Landscape.io, CodeClimate and Houndci.
0) Formulate the problem. Be sure you can concisely and clearly state what it is you want the program to do. If you’re writing something for somebody else, make sure not to move on until all parties can agree on such a description. Developer Lilin Wang emphasizes that you should scope such questions as what kind of device will the program run on, what is the computing speed of the device, what are the user interactions, etc.
1) Establish Proof of Correctness. Don’t code anything until you’re reasonably certain your algorithm works. This includes doing your best to consider all corner cases. Nate Jenkins, co-founder of Authorea (ff Venture Capital company), offers, “Sometimes solving a simpler problem can be a beachhead towards a fully-featured solution. This is especially true for situations with many edge cases.”
2) Make sure it’s efficient enough, if efficiency is a concern. For 95% of the things you’re writing, you don’t need to worry about time or space complexity up front. Just stop to think if this is a performance critical feature. If so, then make sure the space and time complexity will be satisfactory. In the majority of cases, you can move right on ahead.
If efficiency is a concern, run some quick calculations on expected input sizes and time complexity to figure out what order of magnitude of operations it will take. Then figure out if it’s worth brainstorming more efficient solutions. Don’t waste time here — if something will have an input size of at most 25, it’s not worth improving on a linear time solution. If your solution isn’t quite as fast as you need it to be, formulate a better algorithm and return to step 1.
Nate Jenkins writes, “Most of the time it is hard to predict what will be your bottleneck in a complex application. Run a profiler and have it tell you what is slow rather than waste time up-front guessing what is going to be slow. Learn about n+1 queries and how to avoid them if necessary. If something is slow to compute but re-used many times, cache it. If you cannot avoid a slow task, such as an external API call, background it.”
3) Design the code. Make sure you’ve laid out the necessary classes, data and program flow. For even fairly complex problems, skipping or fudging this step can be costly. “Hours of coding can save you minutes of thinking.”
Max Segan, Facebook engineer and former ff Venture Capital intern, observes, “Specifically, think through your API. What should a method take, and what should it return? Are we interacting with mutable state so our functions are further from pure functions? Are we reaching out to global or other state that could change? Let’s write an API that keeps it simple. Are we taking too many arguments, or are we introducing concepts that are out of scope? Example of the latter may be a “Person” object becoming aware of a “teacherID”, because there is a subclass that can be teacher. Understand your scope. Keep it clean and testable.”
He adds, “Once you have your API, but before you write your implementation, is the prime time to add tests. You aren’t biased by how you’re going to implement, so you’ll be true to the API. Think through the edge cases, and write them up. This lets you implement and test it as you go! Implementation can come last (or second to last, as I still definitely advocate code review). I love Differential for code review.”
Nate Jenkins comments, “Picture the dependency graph of your codebase, arrows point from module (or class) A to module B when module A depends on module B. If there are lots of arrows pointing in both directions you have a problem, you cannot easily change one of them without changing the other. This problem gets amplified as your team grows larger. You can often flip the direction of an arrow by inverting a relationship. For example, if you have an Organization class that can have Users as members, you might initially write something like ‘user.is_member_of?(organization)” but you can just as easily write this as ‘organization.is_member?(user)’ or something similar.”
4) Code and document it as you go. If it’s to be integrated into a larger system, confirm the form input and output should take. And always comment your code!
However, Max Segan observes, “Code and Document as you go is nice if you can stick to it. I’m guilty of not commenting. We’ve found that comments tend to degrade, and no comments are better than wrong ones.”
5) Code review and test. Try sample inputs of all kinds, null input, large inputs, repeated values, improperly formatted input, and any corner cases you can think of. Try your best to break it; better now than later.