LGTM: Writing good enough code
This post is a written version of the internal talk I gave on writing good enough code
It is aimed at folks who are writing production code1 for the first time.
If you have been writing software for a while then you probably know these things, and you are not the target audience for this post 🙂
While doing code reviews with folks writing production code for the first time I observed few common suggestions, and later I gave an internal talk on the subject. This post is to written version of the talk.
Please keep in mind that these are some of the guidelines(not rules) one can keep in mind to write good enough code, and these are not complete in any way
What do I mean by good enough code?
Code which is easy to understand and gets the job done is good enough, code that meets the requirements is good enough, code that we can ship is good enough.
I believe mindful shipping beats perfection, and we can always iterate to make it better in future.
Our goal should be shipping as fast as we can while trying not to accumulate tech or product debt.
No Code
- Best code is code you don’t have, code that we don’t write will not have bugs, will not crash or fail, this is the reason why I love PRs that delete more code then they add
“Every line of code you write is time debt: it is another line that has to be debugged, another line that has to be supported, another line that may require a rewrite later, another line that might cause an interaction with a later feature”
PS: Not to be confused with no-code movement
DRY
- “Do not Repeat Yourself” (DRY) with some WET code is considered a good practice
- Keep things DRY, but don’t DRY too much because then it starts getting hard to read and reason about it.
- DRY but don’t compromise readability to DRY something, readability takes precedence over DRYing in my books
Further Reading:
YAGNI
- YAGNI - “You aren’t gonna need it”
- Don’t assume you will need it (unless you are sure, and is part is requirements)
- Don’t speculate how It would be used in far future and just write code that gets the job done for the current use-case
Single Responsibility
The Single Responsibility Principle (SRP) states that every class or module in a program should have responsibility for a single piece of that program’s functionality.
It means don’t write a method that does multiple unrelated things, for example, logging modules have a method to format dates, and that formatting feature is used all over the place.
And the same goes for methods, don’t write something that does more than two things. If you need to add some feature in method and when you feel it’s doing two things, refactor it
Write for Readability
If you only want to follow one thing, make it this, because readability matters, and the simple reason is that code is read more then it’s modified, and readable code will be easy to reason about, change, or replace when required.
“Programs should be written for people to read, and only incidentally for machines to execute.”
More time will be spent reading the code you write, you and your co-workers will be reading more code then you will write
Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.
— John Woods
Further Reading:
- Write code that’s easy to delete, and easy to debug too.
- Writing readable source code
- 10 practices for readable code
- How do you write readable code?: 13 Principles
No Deep Nesting
Deeply nested code is hard to read and hard to understand, it becomes very hard when you have to reason about it or debug it (extra hard when you are in middle of an outage), and as we mentioned readability matters, that’s why deep nesting is considered a code smell
A method with multiple nested if condition quickly becomes hard to comprehend, so try to keep nesting under control.
Use Guard Clauses to get rid of nested conditionals, and keep the branching factor low
Happy Path
The happy path is the optimistic path, It’s the path your code will take under normal conditions, It’s scenario featuring no exceptions or error conditions.
For example, validating input in the start and failing fast will allow you to write code which can work with the assumption that data (down the line) it has it valid since it was validated at the start.
It makes code more readable and easy to debug, also less branching because now you don’t need to handle invalid data
Fail Fast, Fail Early and Fail With Noise
Write your software such that, when there is a problem, it should fail as soon as it detects the problem and knows it can’t recover from it.
Failure should be visible to operator, and failing early is better than trying to proceed in a possibly unstable state where bad state trickles down.
Silent failures are one of the hardest kind of failure to debug, trace and fix, these failures leave no traces for operators, and leave people banging their heads against the wall
You should always leave a visible trace of your failure, raising an exception or logging based on the type of failures
Further Reading:
Shameless plug: I curate failuremodes.dev and failure modes newsletter, do check it out if you interested in how and why software systems fail
Return Single Type
One function should return only one type, eg. only string, hash, or floats. not float for one set of input and string for another set of input
If your function is returning different data types for different inputs then you might want to break it down into multiple functions or unify the returned data type.
Having multiple return types make it hard to test, and it can mean that we are not following single responsibility principle.
Multiple return type is considered a code smell, and should be avoided
Refactoring
It should not be left for later, refactor whenever you can, and when it feels the right thing to do.
Refactoring in small dosages keeps tech debt in check.
Closing Note
You want to ship faster but ship mindfully, and to ship faster you want to get your changes merged without lots of back and forth. You can do that by thinking and reasoning about how and why you are writing something before you start writing something.
Code less, engineer more by Liz Fong-Jones explains this very well, and It’s a must-read, this is something that I follow, and I try to not start coding till I understand the Why, What and How for the change
I suggest doing one round of code review yourself before the code review. I call this self-review, and I do it for all of my pull requests, docs, or anything that I share, and I always find things to improve, and I am sure you will also find things to improve too.
We should keep in mind that, these are guidelines, and they should not be taken to the extreme, our goal is to find a good balance, instead of bike-shading over these things guidelines, and we should focus on things that matter
Our goal is to solve problems and create value. our users don’t care if we are serving them using PHP, and jQuery or the hot new magical framework, as long as these things are not hindering user experience, your users don’t care.
Further Reading:
- What is good code? A scientific definition
- 7 Tips on Writing Better and More Readable Code
- Google Engineering Practices Documentation
- How to do a code review
- Code Health: Respectful Reviews == Useful Reviews
- You’re not writing code, you’re solving problems
- Don’t Shave That Yak!
- All the Little Things by Sandi Metz
- Hammock Driven Development - Rich Hickey
footnotes:
-
any code that’s serving customers in any way (directly or indirectly) is production code in my books ↩